渲染列表 | Rendering Lists

You will often want to display multiple similar components from a collection of data. You can use the JavaScript array methods to manipulate an array of data. On this page, you’ll use filter() and map() with React to filter and transform your array of data into an array of components. 你可能经常需要通过 JavaScript 的数组方法 来操作数组中的数据,从而将一个数据集渲染成多个相似的组件。在这篇文章中,你将学会如何在 React 中使用 filter() 筛选需要渲染的组件和使用 map() 把数组转换成组件数组。

你将会学习到

  • How to render components from an array using JavaScript’s map()
  • 如何通过 JavaScript 的 map() 方法从数组中生成组件
  • How to render only specific components using JavaScript’s filter()
  • 如何通过 JavaScript 的 filter() 筛选需要渲染的组件
  • When and why to use React keys
  • 何时以及为何使用 React 中的 key

从数组中渲染数据 | Rendering data from arrays

Say that you have a list of content. 这里我们有一个列表。

<ul>
<li>凯瑟琳·约翰逊: 数学家</li>
<li>马里奥·莫利纳: 化学家</li>
<li>穆罕默德·阿卜杜勒·萨拉姆: 物理学家</li>
<li>珀西·莱温·朱利亚: 化学家</li>
<li>苏布拉马尼扬·钱德拉塞卡: 天体物理学家</li>
</ul>

The only difference among those list items is their contents, their data. You will often need to show several instances of the same component using different data when building interfaces: from lists of comments to galleries of profile images. In these situations, you can store that data in JavaScript objects and arrays and use methods like map() and filter() to render lists of components from them. 可以看到,这些列表项之间唯一的区别就是其中的内容/数据。未来你可能会碰到很多类似的情况,在那些场景中,你想基于不同的数据渲染出相似的组件,比如评论列表或者个人资料的图库。在这样的场景下,可以把要用到的数据存入 JavaScript 对象或数组,然后用 map()filter() 这样的方法来渲染出一个组件列表。

Here’s a short example of how to generate a list of items from an array: 这里给出一个由数组生成一系列列表项的简单示例:

  1. Move the data into an array:
  • 首先,把数据 存储 到数组中:
const people = [
'凯瑟琳·约翰逊: 数学家',
'马里奥·莫利纳: 化学家',
'穆罕默德·阿卜杜勒·萨拉姆: 物理学家',
'珀西·莱温·朱利亚: 化学家',
'苏布拉马尼扬·钱德拉塞卡: 天体物理学家',
];
  1. Map the people members into a new array of JSX nodes, listItems:
  • 遍历 people 这个数组中的每一项,并获得一个新的 JSX 节点数组 listItems
const listItems = people.map(person => <li>{person}</li>);
  1. Return listItems from your component wrapped in a <ul>:
  • listItems<ul> 包裹起来,然后 返回 它:
return <ul>{listItems}</ul>;

Here is the result: 来看看运行的结果:

const people = [
  '凯瑟琳·约翰逊: 数学家',
  '马里奥·莫利纳: 化学家',
  '穆罕默德·阿卜杜勒·萨拉姆: 物理学家',
  '珀西·莱温·朱利亚: 化学家',
  '苏布拉马尼扬·钱德拉塞卡: 天体物理学家',
];

export default function List() {
  const listItems = people.map(person =>
    <li>{person}</li>
  );
  return <ul>{listItems}</ul>;
}

Notice the sandbox above displays a console error: 注意上面的沙盒可能会输出这样一个控制台错误:

Console
Warning: Each child in a list should have a unique “key” prop.

Warning: Each child in a list should have a unique “key” prop. 等会我们会学到怎么修复它。在此之前,我们先来看看如何把这个数组变得更加结构化。

对数组项进行过滤 | Filtering arrays of items

This data can be structured even more. 让我们把 people 数组变得更加结构化一点。

const people = [{
id: 0,
name: '凯瑟琳·约翰逊',
profession: '数学家',
}, {
id: 1,
name: '马里奥·莫利纳',
profession: '化学家',
}, {
id: 2,
name: '穆罕默德·阿卜杜勒·萨拉姆',
profession: '物理学家',
}, {
id: 3,
name: '珀西·莱温·朱利亚',
profession: '化学家',
}, {
id: 4,
name: '苏布拉马尼扬·钱德拉塞卡',
profession: '天体物理学家',
}];

Let’s say you want a way to only show people whose profession is 'chemist'. You can use JavaScript’s filter() method to return just those people. This method takes an array of items, passes them through a “test” (a function that returns true or false), and returns a new array of only those items that passed the test (returned true). 现在,假设你只想在屏幕上显示职业是 化学家 的人。那么你可以使用 JavaScript 的 filter() 方法来返回满足条件的项。这个方法会让数组的子项经过 “过滤器”(一个返回值为 truefalse 的函数)的筛选,最终返回一个只包含满足条件的项的新数组。

You only want the items where profession is 'chemist'. The “test” function for this looks like (person) => person.profession === 'chemist'. Here’s how to put it together: 既然你只想显示 profession 值是 化学家 的人,那么这里的 “过滤器” 函数应该长这样:(person) => person.profession === '化学家'。下面我们来看看该怎么把它们组合在一起:

  1. Create a new array of just “chemist” people, chemists, by calling filter() on the people filtering by person.profession === 'chemist':
  • 首先,创建 一个用来存化学家们的新数组 chemists,这里用到 filter() 方法过滤 people 数组来得到所有的化学家,过滤的条件应该是 person.profession === '化学家'
const chemists = people.filter(person =>
person.profession === '化学家'
);
  1. Now map over chemists:
  • 接下来 用 map 方法遍历 chemists 数组:
const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
{person.accomplishment}而闻名世界
</p>
</li>
);
  1. Lastly, return the listItems from your component:
  • 最后,返回 listItems
return <ul>{listItems}</ul>;
import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const chemists = people.filter(person =>
    person.profession === '化学家'
  );
  const listItems = chemists.map(person =>
    <li>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}{person.accomplishment}而闻名世界
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}

陷阱

Arrow functions implicitly return the expression right after =>, so you didn’t need a return statement: 因为箭头函数会隐式地返回位于 => 之后的表达式,所以你可以省略 return 语句。

const listItems = chemists.map(person =>
<li>...</li> // 隐式地返回!
);

However, you must write return explicitly if your => is followed by a { curly brace! 不过,如果你的 => 后面跟了一对花括号 { ,那你必须使用 return 来指定返回值!

const listItems = chemists.map(person => { // 花括号
return <li>...</li>;
});

Arrow functions containing => { are said to have a “block body”. They let you write more than a single line of code, but you have to write a return statement yourself. If you forget it, nothing gets returned! 箭头函数 => { 后面的部分被称为 “块函数体”,块函数体支持多行代码的写法,但要用 return 语句才能指定返回值。假如你忘了写 return,那这个函数什么都不会返回!

key 保持列表项的顺序 | Keeping list items in order with key

Notice that all the sandboxes above show an error in the console: 如果把上面任何一个沙盒示例在新标签页打开,你就会发现控制台有这样一个报错:

Console
Warning: Each child in a list should have a unique “key” prop.

You need to give each array item a key — a string or a number that uniquely identifies it among other items in that array: 这是因为你必须给数组中的每一项都指定一个 key——它可以是字符串或数字的形式,只要能唯一标识出各个数组项就行:

<li key={person.id}>...</li>

注意

JSX elements directly inside a map() call always need keys! 直接放在 map() 方法里的 JSX 元素一般都需要指定 key 值!

Keys tell React which array item each component corresponds to, so that it can match them up later. This becomes important if your array items can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key helps React infer what exactly has happened, and make the correct updates to the DOM tree. 这些 key 会告诉 React,每个组件对应着数组里的哪一项,所以 React 可以把它们匹配起来。这在数组项进行移动(例如排序)、插入或删除等操作时非常重要。一个合适的 key 可以帮助 React 推断发生了什么,从而得以正确地更新 DOM 树。

Rather than generating keys on the fly, you should include them in your data: 用作 key 的值应该在数据中提前就准备好,而不是在运行时才随手生成:

export const people = [
  {
    id: 0, // 在 JSX 中作为 key 使用 // Used in JSX as a key
    name: '凯瑟琳·约翰逊',
    profession: '数学家',
    accomplishment: '太空飞行相关数值的核算',
    imageId: 'MK3eW3A',
  },
  {
    id: 1, // 在 JSX 中作为 key 使用 // Used in JSX as a key
    name: '马里奥·莫利纳',
    profession: '化学家',
    accomplishment: '北极臭氧空洞的发现',
    imageId: 'mynHUSa',
  },
  {
    id: 2, // 在 JSX 中作为 key 使用 // Used in JSX as a key
    name: '穆罕默德·阿卜杜勒·萨拉姆',
    profession: '物理学家',
    accomplishment: '关于基本粒子间弱相互作用和电磁相互作用的统一理论',
    imageId: 'bE7W1ji',
  },
  {
    id: 3, // 在 JSX 中作为 key 使用 // Used in JSX as a key
    name: '珀西·莱温·朱利亚',
    profession: '化学家',
    accomplishment: '开创性的可的松药物、类固醇和避孕药',
    imageId: 'IOjWm71',
  },
  {
    id: 4, // 在 JSX 中作为 key 使用 // Used in JSX as a key
    name: '苏布拉马尼扬·钱德拉塞卡',
    profession: '天体物理学家',
    accomplishment: '白矮星质量计算',
    imageId: 'lrWQx8l',
  },
];

深入探讨

为每个列表项显示多个 DOM 节点 | Displaying several DOM nodes for each list item

What do you do when each item needs to render not one, but several DOM nodes? 如果你想让每个列表项都输出多个 DOM 节点而非一个的话,该怎么做呢?

The short <>...</> Fragment syntax won’t let you pass a key, so you need to either group them into a single <div>, or use the slightly longer and more explicit <Fragment> syntax: Fragment 语法的简写形式 <> </> 无法接受 key 值,所以你只能要么把生成的节点用一个 <div> 标签包裹起来,要么使用长一点但更明确的 <Fragment> 写法:

import { Fragment } from 'react';

// ...

const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);

Fragments disappear from the DOM, so this will produce a flat list of <h1>, <p>, <h1>, <p>, and so on. 这里的 Fragment 标签本身并不会出现在 DOM 上,这串代码最终会转换成 <h1><p><h1><p>…… 的列表。

如何设定 key 值 | Where to get your key

Different sources of data provide different sources of keys: 不同来源的数据往往对应不同的 key 值获取方式:

  • Data from a database: If your data is coming from a database, you can use the database keys/IDs, which are unique by nature.
  • 来自数据库的数据: 如果你的数据是从数据库中获取的,那你可以直接使用数据表中的主键,因为它们天然具有唯一性。
  • Locally generated data: If your data is generated and persisted locally (e.g. notes in a note-taking app), use an incrementing counter, crypto.randomUUID() or a package like uuid when creating items.
  • 本地产生数据: 如果你数据的产生和保存都在本地(例如笔记软件里的笔记),那么你可以使用一个自增计数器或者一个类似 uuid 的库来生成 key。

key 需要满足的条件 | Rules of keys

  • Keys must be unique among siblings. However, it’s okay to use the same keys for JSX nodes in different arrays.
  • key 值在兄弟节点之间必须是唯一的。 不过不要求全局唯一,在不同的数组中可以使用相同的 key。
  • Keys must not change or that defeats their purpose! Don’t generate them while rendering.
  • key 值不能改变,否则就失去了使用 key 的意义!所以千万不要在渲染时动态地生成 key。

React 中为什么需要 key? | Why does React need keys?

Imagine that files on your desktop didn’t have names. Instead, you’d refer to them by their order — the first file, the second file, and so on. You could get used to it, but once you delete a file, it would get confusing. The second file would become the first file, the third file would be the second file, and so on. 设想一下,假如你桌面上的文件都没有文件名,取而代之的是,你需要通过文件的位置顺序来区分它们———第一个文件,第二个文件,以此类推。也许你也不是不能接受这种方式,可是一旦你删除了其中的一个文件,这种组织方式就会变得混乱无比。原来的第二个文件可能会变成第一个文件,第三个文件会成为第二个文件……

File names in a folder and JSX keys in an array serve a similar purpose. They let us uniquely identify an item between its siblings. A well-chosen key provides more information than the position within the array. Even if the position changes due to reordering, the key lets React identify the item throughout its lifetime. React 里需要 key 和文件夹里的文件需要有文件名的道理是类似的。它们(key 和文件名)都让我们可以从众多的兄弟元素中唯一标识出某一项(JSX 节点或文件)。而一个精心选择的 key 值所能提供的信息远远不止于这个元素在数组中的位置。即使元素的位置在渲染的过程中发生了改变,它提供的 key 值也能让 React 在整个生命周期中一直认得它。

陷阱

You might be tempted to use an item’s index in the array as its key. In fact, that’s what React will use if you don’t specify a key at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs. 你可能会想直接把数组项的索引当作 key 值来用,实际上,如果你没有显式地指定 key 值,React 确实默认会这么做。但是数组项的顺序在插入、删除或者重新排序等操作中会发生改变,此时把索引顺序用作 key 值会产生一些微妙且令人困惑的 bug。

Similarly, do not generate keys on the fly, e.g. with key={Math.random()}. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data. 与之类似,请不要在运行过程中动态地产生 key,像是 key={Math.random()} 这种方式。这会导致每次重新渲染后的 key 值都不一样,从而使得所有的组件和 DOM 元素每次都要重新创建。这不仅会造成运行变慢的问题,更有可能导致用户输入的丢失。所以,使用能从给定数据中稳定取得的值才是明智的选择。

Note that your components won’t receive key as a prop. It’s only used as a hint by React itself. If your component needs an ID, you have to pass it as a separate prop: <Profile key={id} userId={id} />. 有一点需要注意,组件不会把 key 当作 props 的一部分。Key 的存在只对 React 本身起到提示作用。如果你的组件需要一个 ID,那么请把它作为一个单独的 prop 传给组件: <Profile key={id} userId={id} />

摘要

On this page you learned: 在这篇文章中,你学习了:

  • How to move data out of components and into data structures like arrays and objects.
  • 如何从组件中抽离出数据,并把它们放入像数组、对象这样的数据结构中。
  • How to generate sets of similar components with JavaScript’s map().
  • 如何使用 JavaScript 的 map() 方法来生成一组相似的组件。
  • How to create arrays of filtered items with JavaScript’s filter().
  • 如何使用 JavaScript 的 filter() 方法来筛选数组。
  • Why and how to set key on each component in a collection so React can keep track of each of them even if their position or data changes.
  • 为何以及如何给集合中的每个组件设置一个 key 值:它使 React 能追踪这些组件,即便后者的位置或数据发生了变化。

1挑战 4 个挑战:
把列表一分为二 | Splitting a list in two

This example shows a list of all people. 下面的示例中有一个包含所有人员信息的列表。

Change it to show two separate lists one after another: Chemists and Everyone Else. Like previously, you can determine whether a person is a chemist by checking if person.profession === 'chemist'. 请试着把它分成一前一后的两个列表:分别是 化学家们其余的人。像之前一样,你可以通过 person.profession === '化学家' 这个条件来判断一个人是不是化学家。

import { people } from './data.js';
import { getImageUrl } from './utils.js';

export default function List() {
  const listItems = people.map(person =>
    <li key={person.id}>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}{person.accomplishment}而闻名世界
      </p>
    </li>
  );
  return (
    <article>
      <h1>科学家</h1>
      <ul>{listItems}</ul>
    </article>
  );
}