目录

第九章:列表渲染

9.1 基础列表渲染

使用 JavaScript 的 map 方法将数组转换为元素列表。

function NumberList({ numbers }) {
  const listItems = numbers.map((number) =>
    <li>{number}</li>
  );
 
  return <ul>{listItems}</ul>;
}
 
// 更简洁的写法
function NumberList({ numbers }) {
  return (
    <ul>
      {numbers.map(number => <li>{number}</li>)}
    </ul>
  );
}
 
// 使用
const numbers = [1, 2, 3, 4, 5];
<NumberList numbers={numbers} />

9.2 Key 的重要性

Key 帮助 React 识别哪些元素改变了、添加了或删除了。

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>;
  );
}

选择 Key 的最佳实践

为什么不能用索引作为 Key

// 错误示例
function List({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <input key={index} value={item.value} />
      ))}
    </ul>;
  );
}
 
// 问题:删除第一个元素后,所有元素的 key 都变了
// 导致 React 重新渲染所有输入框,丢失焦点状态

9.3 提取列表组件

function ListItem({ item }) {
  return <li>{item.text}</li>;
}
 
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <ListItem key={todo.id} item={todo} />
      ))}
    </ul>;
  );
}

注意:key 应该放在数组上下文的元素上,而不是组件内部。

9.4 嵌套列表

function CategoryList({ categories }) {
  return (
    <div>
      {categories.map(category => (
        <div key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>;
  );
}

9.5 列表过滤和排序

过滤列表

function FilteredList({ items, filterText }) {
  const filtered = items.filter(item =>
    item.name.toLowerCase().includes(filterText.toLowerCase())
  );
 
  return (
    <ul>
      {filtered.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>;
  );
}

排序列表

function SortedList({ items, sortBy }) {
  const sorted = [...items].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    }
    if (sortBy === 'date') {
      return new Date(b.date) - new Date(a.date);
    }
    return 0;
  });
 
  return (
    <ul>
      {sorted.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>;
  );
}

9.6 虚拟列表

当列表项很多时,使用虚拟列表只渲染可视区域的内容。

使用 react-window

npm install react-window
import { FixedSizeList as List } from 'react-window';
 
function VirtualList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>;
  );
 
  return (
    <List
      height={500}
      itemCount={items.length}
      itemSize={35}
    >
      {Row}
    </List>;
  );
}

使用 react-virtualized

npm install react-virtualized
import { List } from 'react-virtualized';
 
function VirtualizedList({ items }) {
  const rowRenderer = ({ key, index, style }) => (
    <div key={key} style={style}>
      {items[index].name}
    </div>;
  );
 
  return (
    <List
      width={300}
      height={500}
      rowCount={items.length}
      rowHeight={50}
      rowRenderer={rowRenderer}
    />;
  );
}

9.7 无限滚动

import { useState, useEffect, useRef, useCallback } from 'react';
 
function InfiniteList() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const observer = useRef();
 
  const lastItemRef = useCallback(node => {
    if (loading) return;
    if (observer.current) observer.current.disconnect();
 
    observer.current = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) {
        setPage(prevPage => prevPage + 1);
      }
    });
 
    if (node) observer.current.observe(node);
  }, [loading]);
 
  useEffect(() => {
    setLoading(true);
    fetchItems(page).then(newItems => {
      setItems(prev => [...prev, ...newItems]);
      setLoading(false);
    });
  }, [page]);
 
  return (
    <div>
      {items.map((item, index) => (
        <div
          key={item.id}
          ref={index === items.length - 1 ? lastItemRef : null}
        >
          {item.name}
        </div>;
      ))}
      {loading && <p>加载中...</p>}
    </div>;
  );
}

9.8 列表动画

使用 react-flip-toolkit

npm install react-flip-toolkit
import { Flipper, Flipped } from 'react-flip-toolkit';
 
function AnimatedList({ items }) {
  return (
    <Flipper flipKey={items.map(i => i.id).join(',')}>
      <ul>
        {items.map(item => (
          <Flipped key={item.id} flipId={item.id}>
            <li>{item.name}</li>
          </Flipped>;
        ))}
      </ul>;
    </Flipper>;
  );
}

使用 framer-motion

npm install framer-motion
import { motion, AnimatePresence } from 'framer-motion';
 
function AnimatedList({ items, onRemove }) {
  return (
    <AnimatePresence>
      {items.map(item => (
        <motion.li
          key={item.id}
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, x: -100 }}
          onClick={() => onRemove(item.id)}
        >
          {item.name}
        </motion.li>;
      ))}
    </AnimatePresence>;
  );
}

9.9 总结

本章介绍了列表渲染的各种技术:

处理列表是 React 开发的常见场景,掌握这些技巧对于构建高性能应用非常重要。