你經常會需要用多個相似的 component 來顯示一系列的資料。這個時候你可以在 React 中使用 JavaScript 的 filter()map() 來過濾資料並轉換成包含多個 component 的 array。

You will learn

  • 如何使用 JavaScript 的 map() 方法處理 array 資料 render component
  • 如何使用 JavaScript 的 filter() 方法處理 array 資料 render 特定的 component
  • 什麼時候使用及為什麼使用 React 中的 key

從 array 中 render 資料

假設你有一個內容列表。

<ul>
<li>Creola Katherine Johnson: mathematician</li>
<li>Mario José Molina-Pasquel Henríquez: chemist</li>
<li>Mohammad Abdus Salam: physicist</li>
<li>Percy Lavon Julian: chemist</li>
<li>Subrahmanyan Chandrasekhar: astrophysicist</li>
</ul>

在這個列表唯一的差異就在於各個內容和資料。當你建立介面時,常常需要使用不同資料顯示相同的 component,從評論列表到個人資料圖庫。在這些情況下,你可以將資料儲存在 JavaScript 的 object 和array 中,並使用 map()filter() 的方法來 render 一個 component 列表。

這裡有個使用 array 產生一個列表項目的簡單範例:

  1. 將資料移入陣列:
const people = [
'Creola Katherine Johnson: mathematician',
'Mario José Molina-Pasquel Henríquez: chemist',
'Mohammad Abdus Salam: physicist',
'Percy Lavon Julian: chemist',
'Subrahmanyan Chandrasekhar: astrophysicist'
];
  1. people 成員進行 Map,以建立新的 JSX 節點 array,listItems
const listItems = people.map(person => <li>{person}</li>);
  1. listItems<ul> 包起來,並且回傳它:
return <ul>{listItems}</ul>;

來看看運作結果:

const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist',
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];

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

請注意上面的範例中 sandbox 顯示了一個錯誤訊息:

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

你將會在這個頁面稍後學到如何修正這個錯誤。在這之前,我們先將這個 array 資料更加結構化。

過濾 array 裡的項目

這讓資料變得更結構化。

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',
}, {
name: 'Percy Lavon Julian',
profession: 'chemist',
}, {
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
}];

假設你想要一種方法,僅顯示其職業為「chemist」的人。你可以使用 JavaScript 的 filter() 方法來篩選符合條件的項目。這個方法會讓 array 中的每個項目進行「過濾」(一個回傳 truefalse 的函式),最後回傳一個滿足篩選條件項目的新 array。

你只想要 profession'chemist' 的項目。此「測試」函式看起來像 (person) => person.profession === 'chemist'。以下是如何將它們組合起來的方式:

  1. 透過在 people 上呼叫 filter()person.profession === 'chemist' 為過濾條件,建立一個僅包含「化學家」的新 chemists array:
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. 最後,從你的 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 === '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> // Implicit return!
);

然而,如果你的 => 後面跟著 { 大括號,那你必須明確地撰寫 return 陳述式!

const listItems = chemists.map(person => { // Curly brace
return <li>...</li>;
});

包含 => { 的箭頭函式被稱為「區塊主體函式」。它們讓你可以撰寫多行程式碼,但你必須自己撰寫 return 陳述式。如果你忘了寫,函式就不會回傳任何值!詳細說明可以參考這裡

使用 key 保持列表項目的順序

請注意上面所有的範例,所有的 sandbox 都顯示了一個錯誤訊息:

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

你必須給 array 裡的每一個項目指定一個 key — 它可以是 string 或是 number,可以在 array 的其他項目中唯一的識別它。

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

Note

map() 方法裡呼叫 JSX elements 總是需要指定 key!

這些 key 會告訴 React ,每個 component 對應著 array 裡的哪一項,React 就可以把它們對應起來。這在某些需要被操作的 array 上非常重要,例如排序、新增或是刪除資料等。一個合適的 key 可以幫助 React 推斷發生什麼事,從而正確的更新 DOM Tree。

與其即時產生 key,你應該將它們包含在你的資料中:

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

Deep Dive

為每個列表顯示多個 DOM nodes

如果你想讓每個列表項目都 render 多個 DOM nodes 而不是一個的話,你該怎麼做?

<>...</> 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:

  • **來自資料庫的資料:**如果你的資料是來自於資料庫,你可以使用資料庫的 keys/IDs ,它們本身就具有唯一性。
  • **本機產生的資料:**如果你資料的產生和儲存都在本機 (例如筆記軟體的筆記),那麼你可使用計數器、crypto.randomUUID() 或是像 uuid 的套件來產生這些 key 值。

Key 的規則

  • Siblings 之間的 key 必須是唯一的。 然而,在_不同_的 array 中使用相同的 key,對於 JSX node 是可以的。
  • Key 不能改變否則就失去使用它的意義!所以不要在 rendering 時動態產生它們。

為什麼 React 需要 key?

想像你的桌面上沒有檔案名稱。相反的,而是按順序來區分它們 — 第一個檔案、第二個檔案等等。你可能會慢慢習慣,但一旦刪除了檔案,就會變得很混亂。第二個檔案就會變成第一個檔案,第三個檔案就會變成第二個檔案,以此類推。

在資料夾中的檔案名稱和 array 中的 JSX key 有類似的功能。它們讓我們可以在 siblings 之間識別一個項目。一個選擇得當的 key 提供的訊息比array 中的索引來得更好。即使_位置_由於重新排序而改變,key 也可以讓 React 在其生命週期中識別該項目。

Pitfall

你可能會想用 array 中的索引值來當作 key 值,實際上,當你沒有指定 key 值時,React 會使用它。但是,如果插入、刪除項目或重新排序 array,你 render 項目的順序將會改變。索引作為 key 往往會導致微妙且令人困惑的錯誤。

類似的概念還有不要隨機產生 key,例如像是 key={Math.random()}。這會導致 key 在 render 時永遠無法 match,導致所有 component 和 DOM 每次都會被重新建立。不僅速度慢,而且會失去項目列表內使用者的輸入。取而代之的是使用基於資料的穩定 ID。

請注意 component 不會將 key 當成 props 的一部分。key 的存在只用於提示 React 本身。如果你的 component 需要一個 ID,那麼請另外傳送 prop 給 component: <Profile key={id} userId={id} />

Recap

在這篇文章中,你學會了:

  • 如何從 component 抽出資料,並把它們放入像是 array 或是 object 結構中。
  • 如何使用 JavaScript 的 map 方法來產生一組相似的 component。
  • 如何使用 JavaScript 的 filter 方法來過濾 array。
  • 為什麼以及如何在集合中的每個 component 設定 key,使 React 能在資料位置被改變或是發生變化時,能持續追蹤這些 component。

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>
  );
}