渲染列表

你经常会想要从一组数据中显示多个相似的组件。你可以使用 JavaScript 数组方法 来操作一组数据。在本页中,你将结合 React 使用 filter()map() 来过滤并将你的数据数组转换为组件数组。

You will learn

  • 如何使用 JavaScript 的 map() 从数组中渲染组件
  • 如何使用 JavaScript 的 filter() 只渲染特定组件
  • 何时以及为何使用 React keys

从数组中渲染数据

假设你有一组内容。

<ul>
<li>Creola Katherine Johnson:数学家</li>
<li>Mario José Molina-Pasquel Henríquez:化学家</li>
<li>Mohammad Abdus Salam:物理学家</li>
<li>Percy Lavon Julian:化学家</li>
<li>Subrahmanyan Chandrasekhar:天体物理学家</li>
</ul>

这些列表项之间唯一的区别是它们的内容,也就是它们的数据。在构建界面时,你经常需要使用不同的数据来显示同一个组件的多个实例:从评论列表到个人资料图片画廊。在这些情况下,你可以将这些数据存储在 JavaScript 对象和数组中,并使用像 map()filter() 这样的函数从中渲染组件列表。

下面是一个简短示例,说明如何从数组生成项目列表:

  1. 数据移动到数组中:
const people = [
'Creola Katherine Johnson:数学家',
'Mario José Molina-Pasquel Henríquez:化学家',
'Mohammad Abdus Salam:物理学家',
'Percy Lavon Julian:化学家',
'Subrahmanyan Chandrasekhar:天体物理学家'
];
  1. 使用 map()people 成员映射到一个新的 JSX 节点数组 listItems
const listItems = people.map(person => <li>{person}</li>);
  1. 你的组件中返回<ul> 包裹的 listItems
return <ul>{listItems}</ul>;

结果如下:

const people = [
  'Creola Katherine Johnson:数学家',
  'Mario José Molina-Pasquel Henríquez:化学家',
  'Mohammad Abdus Salam:物理学家',
  'Percy Lavon Julian:化学家',
  'Subrahmanyan Chandrasekhar:天体物理学家'
];

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

请注意,上面的沙盒显示了一个控制台错误:

Console
警告:列表中的每个子项都应该有一个唯一的 “key” 属性。

你将在本页后面学习如何修复这个错误。在此之前,让我们先为你的数据添加一些结构。

过滤数组中的项目

这组数据可以组织得更加结构化。

const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
}];

假设你想只显示职业是 'chemist' 的人。你可以使用 JavaScript 的 filter() 方法只返回这些人。这个方法接收一个项目数组,让它们通过一个“测试”(一个返回 truefalse 的函数),并返回一个只包含通过测试(返回 true)的项目的新数组。

你只想要 profession'chemist' 的项目。这个“测试”函数看起来像 (person) => person.profession === 'chemist'。下面是如何把它组合起来:

  1. 通过在 people 上调用 filter(),按 person.profession === 'chemist' 过滤,创建 一个只包含“化学家”的新数组 chemists
const chemists = people.filter(person =>
person.profession === 'chemist'
);
  1. 现在对 chemists 进行 map
const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
  1. 最后,从你的组件中返回 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 === 'chemist'
  );
  const listItems = chemists.map(person =>
    <li>
      <img
        src={getImageUrl(person)}
        alt={person.name}
      />
      <p>
        <b>{person.name}:</b>
        {' ' + person.profession + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return <ul>{listItems}</ul>;
}

Pitfall

箭头函数会隐式返回 => 后面的表达式,所以你不需要写 return 语句:

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

然而,如果你的 => 后面跟着一个 { 花括号,你必须显式写出 return

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

包含 => { 的箭头函数被称为具有 “块体”。它们允许你编写不止一行代码,但你必须自己写 return 语句。要是忘了,什么都不会返回!

使用 key 保持列表项顺序

请注意,上面的所有沙盒在控制台中都显示了一个错误:

Console
警告:列表中的每个子项都应该有一个唯一的 “key” 属性。

你需要给每个数组项一个 key——一个字符串或数字,用来在该数组的其他项中唯一标识它:

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

Note

直接写在 map() 调用中的 JSX 元素总是需要 keys!

keys 告诉 React 每个组件对应数组中的哪一项,这样它之后才能把它们匹配起来。如果你的数组项可能移动(例如由于排序)、被插入或被删除,这一点就很重要。一个选择良好的 key 能帮助 React 推断到底发生了什么,并对 DOM 树进行正确的更新。

与其动态生成 key,不如把它们包含在你的数据中:

export const people = [{
  id: 0, // 在 JSX 中用作 key
  name: 'Creola Katherine Johnson',
  profession: 'mathematician',
  accomplishment: 'spaceflight calculations',
  imageId: 'MK3eW3A'
}, {
  id: 1, // 在 JSX 中用作 key
  name: 'Mario José Molina-Pasquel Henríquez',
  profession: 'chemist',
  accomplishment: 'discovery of Arctic ozone hole',
  imageId: 'mynHUSa'
}, {
  id: 2, // 在 JSX 中用作 key
  name: 'Mohammad Abdus Salam',
  profession: 'physicist',
  accomplishment: 'electromagnetism theory',
  imageId: 'bE7W1ji'
}, {
  id: 3, // 在 JSX 中用作 key
  name: 'Percy Lavon Julian',
  profession: 'chemist',
  accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
  imageId: 'IOjWm71'
}, {
  id: 4, // 在 JSX 中用作 key
  name: 'Subrahmanyan Chandrasekhar',
  profession: 'astrophysicist',
  accomplishment: 'white dwarf star mass calculations',
  imageId: 'lrWQx8l'
}];

Deep Dive

为每个列表项显示多个 DOM 节点

如果每个项目需要渲染的不只是一个,而是多个 DOM 节点,你该怎么办?

简短的 <>...</> 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 会从 DOM 中消失,因此这会生成一个由 <h1><p><h1><p> 等组成的扁平列表。

key 从哪里来

不同的数据来源会提供不同来源的 keys:

  • 来自数据库的数据: 如果你的数据来自数据库,你可以使用数据库键/ID,它们本身就是唯一的。
  • 本地生成的数据: 如果你的数据是在本地生成并持久化的(例如记事应用中的笔记),在创建项目时使用递增计数器、crypto.randomUUID() 或像 uuid 这样的包。

keys 的规则

  • Keys 在同级元素之间必须唯一。 但是,在_不同_数组中的 JSX 节点使用相同的 keys 是可以的。
  • Keys 不能改变,否则它们的作用就失效了!不要在渲染时生成它们。

为什么 React 需要 keys?

想象一下,如果你桌面上的文件没有名称。取而代之的是,你只能按照它们的顺序来指代它们——第一个文件、第二个文件,依此类推。你可能会慢慢习惯,但一旦你删除了一个文件,事情就会变得混乱。第二个文件会变成第一个文件,第三个文件会变成第二个文件,依此类推。

文件夹中的文件名和数组中的 JSX keys 起着类似的作用。它们让我们能够在同级项之间唯一地识别某一项。一个选择良好的 key 提供的信息比数组中的位置更多。即使由于重新排序导致_位置_发生变化,key 也能让 React 在其整个生命周期中识别该项。

Pitfall

你可能会想把数组项的索引用作它的 key。事实上,如果你根本不指定 key,React 就会使用这个值。但是,如果某个项目被插入、删除,或者数组顺序被重新排列,你渲染项目的顺序就会随着时间而变化。把索引当作 key 往往会导致细微而令人困惑的 bug。

同样,不要动态生成 keys,例如 key={Math.random()}。这会导致每次渲染之间 keys 都无法匹配,从而使你的所有组件和 DOM 每次都被重新创建。这不仅很慢,还会丢失列表项中的任何用户输入。相反,请使用基于数据的稳定 ID。

注意,你的组件不会把 key 作为 prop 接收到。它只会被 React 自身当作提示使用。如果你的组件需要一个 ID,你必须将其作为单独的 prop 传递:<Profile key={id} userId={id} />

Recap

在本页中你学到了:

  • 如何将数据从组件中移出,并放入数组和对象之类的数据结构中。
  • 如何使用 JavaScript 的 map() 生成一组相似的组件。
  • 如何使用 JavaScript 的 filter() 创建过滤后的项目数组。
  • 为什么以及如何在集合中的每个组件上设置 key,这样即使它们的位置或数据发生变化,React 也能跟踪每个组件。

Challenge 1 of 4:
将一个列表拆成两个

这个示例展示了一个包含所有人的列表。

把它改成依次显示两个单独的列表:化学家其他所有人。 和之前一样,你可以通过检查 person.profession === 'chemist' 来判断某人是否是化学家。

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 + ' '}
        known for {person.accomplishment}
      </p>
    </li>
  );
  return (
    <article>
      <h1>Scientists</h1>
      <ul>{listItems}</ul>
    </article>
  );
}