React Doc | Describing the UI - Rendering Lists

picture

2025-05-02

React Doc | Describing the UI - Rendering Lists

前言

此為 Describing the UI: Unveiling the Power of React Components 此篇文章的延伸內容之一。

這一系列文章是筆者在學習 React 時,閱讀 React 官方文件 所做的翻譯筆記,希望能幫助更多的人學習 React。

筆者會將段落標題的英文原文附上在標題後方,讓有需要的人可以參照。

本篇文章沒有節錄原文範例的輸出結果,如有需要請至原文對照參考。

渲染清單 (Rendering Lists)

你經常會想要根據一組資料來顯示多個相似的元件。你可以使用 JavaScript 的陣列方法 來操作資料的陣列。在這一頁中,你會使用 filter()map() 搭配 React,來篩選與轉換你的資料陣列,將它們變成元件的陣列。

這個章節你將學到:

  • 如何使用 JavaScript 的 map() 從陣列中渲染元件
  • 如何使用 JavaScript 的 filter() 只渲染特定的元件
  • 何時以及為什麼要使用 React 的 key

從陣列渲染資料 (Rendering data from arrays)

假設你有一份內容清單:

<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: mathematician",
      "Mario José Molina-Pasquel Henríquez: chemist",
      "Mohammad Abdus Salam: physicist",
      "Percy Lavon Julian: chemist",
      "Subrahmanyan Chandrasekhar: astrophysicist",
    ];
    
  2. 使用 map 將 people 陣列中的成員轉換成新的 JSX 節點陣列 listItems:

    const listItems = people.map((person) => <li>{person}</li>);
    
  3. 從元件中回傳 包裹著 listItems<ul>

    return <ul>{listItems}</ul>;
    

這是完整的程式碼結果:

(可以到官方文件原文提供的 sandbox 直接操作看看)

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

請注意,上方範例會顯示一個 console 錯誤訊息:

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

你將會在本頁稍後的部分學到如何修正這個錯誤。在那之前,讓我們先為你的資料加上一些結構。

篩選陣列中的項目 (Filtering arrays of items)

這份資料還可以再結構化得更完整一些:

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)的項目。

你只想要職業為 'chemist' 的項目。這個「測試函式」可以寫成 (person) => person.profession === 'chemist'

這是整個流程的寫法:

  1. 建立一個新的陣列 chemists,只包含職業是 chemist 的人,透過對 people 使用 filter(),條件為 person.profession === 'chemist'

    const chemists = people.filter((person) => person.profession === "chemist");
    
  2. 然後對 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>
    ));
    
  3. 最後,從元件中 回傳 listItems

    return <ul>{listItems}</ul>;
    

完整程式碼:

(可以到官方文件原文提供的 sandbox 直接操作看看) App.js

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

data.js

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

utils.js

export function getImageUrl(person) {
  return "https://i.imgur.com/" + person.imageId + "s.jpg";
}

注意事項

箭頭函式在 => 後面直接接表達式時會自動回傳,因此你不需要加上 return

const listItems = chemists.map(
  (person) => <li>...</li> // 自動回傳!
);

但如果你的 => 後面是用 { 大括號包起來的區塊,那你就必須要明確寫出 return

const listItems = chemists.map((person) => {
  // 使用大括號
  return <li>...</li>;
});

這種使用 => {} 的箭頭函式被稱為 「區塊主體」(block body)。它允許你撰寫多行程式碼,但你一定要自己寫 return。如果忘了寫,就會什麼都沒回傳!

使用 key 保持清單項目的順序 (Keeping list items in order with key)

你可能注意到,上面範例的 sandbox 都在開發者工具中出現這個錯誤訊息:

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

你需要為每個陣列項目加上一個 key —— 它必須是在該陣列中能唯一識別該項目的字串或數字:

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

注意:

只要 JSX 元素是直接寫在 map() 裡的,就一定要加上 key

key 的作用是讓 React 能夠知道每個元件對應到陣列中的哪一筆資料,這樣在重新渲染時才能正確比對。如果你的陣列資料有可能會變動(像是排序、插入或刪除),一個設計良好的 key 能幫助 React 判斷哪些元素被變更,並且正確地更新 DOM tree。

與其在渲染時動態產生 key,你應該在資料本身中就包含這個欄位:

完整程式碼:

(可以到官方文件原文提供的 sandbox 直接操作看看)

  1. App.js

    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 <ul>{listItems}</ul>;
    }
    
  2. data.js

    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",
      },
    ];
    
  3. utils.js

    export function getImageUrl(person) {
      return "https://i.imgur.com/" + person.imageId + "s.jpg";
    }
    

💡 深入探討

每個清單項目要顯示多個 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>
));

Fragment 在實際的 DOM 中不會留下痕跡,所以渲染結果會是一個扁平的清單,像是 <h1>, <p>, <h1>, <p>,依此類推。

要從哪裡取得 key

不同來源的資料會提供不同的 key 來源:

  • 來自資料庫的資料: 如果你的資料是從資料庫來的,可以直接使用資料庫的主鍵(keys)或 IDs,因為它們本質上是唯一的(unique by nature)。
  • 本地產生的資料: 如果你的資料是在本地產生並儲存在本地端的(例如筆記應用程式中的筆記),在建立資料時可以使用遞增計數器(incrementing counter)、crypto.randomUUID() 或像 uuid 這類的套件來產生唯一 ID。

使用 key 的規則

  • key 在同一層級中必須是唯一的。 不過,不同陣列中的 JSX 節點可以使用相同的 key
  • key 不應該變動, 否則會失去 key 的作用!請不要在渲染(render)時才動態產生 key

為什麼 React 需要 key

想像一下你的電腦桌面上的檔案都沒有名字,而你只能用順序來辨識它們:第一個檔案、第二個檔案、第三個檔案……這在一開始也許還能應付,但一旦你刪除一個檔案,整個順序就會混亂。第二個檔案會變成第一個,第三個變成第二個,以此類推。

資料夾裡的「檔名」和陣列中的 JSX key 扮演的就是類似的角色:它們讓我們可以在同一層級中獨立辨識每一項目。相較於位置,精心設計的 key 傳遞的是更穩定的識別方式。即使某一項在陣列中的位置因為排序而改變,只要 key 不變,React 就能正確辨識這是同一項目,並維持它的生命週期。

⚠️ 注意事項

你可能會想用陣列中項目的索引作為 key。事實上,如果你沒有指定 key,React 就會自動使用索引。不過,當項目被插入、刪除,或陣列順序被重新排列時,渲染的順序會改變。使用索引作為 key,常常會導致一些難以察覺且令人困惑的 bug。

同樣地,也不要在渲染時即時產生 key,例如使用 key={Math.random()}。這會讓每次渲染時的 key 都對不上,導致所有元件和 DOM 每次都會被重新建立。不僅效能變慢,還會導致列表中原本的使用者輸入遺失。你應該使用根據資料產生的穩定 ID

請注意,元件本身不會接收到 key 作為 prop。它是 React 自己內部使用的提示。如果你的元件需要一個 ID,你必須另外傳入一個 prop,例如:<Profile key={id} userId={id} />

總結

本頁你學到了:

  • 如何將資料從元件中抽離,儲存在像陣列或物件這樣的資料結構中。
  • 如何使用 JavaScript 的 map() 產生一組類似的元件。
  • 如何使用 JavaScript 的 filter() 建立過濾後的陣列。
  • 為什麼要在集合中的每個元件上設定 key,以及如何設定,這樣 React 才能即使在位置或資料變更時,依然正確追蹤每個元件。

試試看一些挑戰 (Try out some challenges)

建議搭配官網提供的小視窗,可以直接在網頁上修改程式碼,並觀察結果。

挑戰 1 / 4:將一個清單分成兩部分

這個範例會顯示所有人的清單。

請修改程式碼,讓它改為依序顯示兩個清單:化學家(Chemists)其他人(Everyone Else)。就像先前一樣,你可以透過判斷 person.profession === 'chemist' 來判斷這個人是否為化學家。

App.js:

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

data.js:

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

挑戰 2 / 4:在一個元件中嵌套清單

從這個陣列中建立食譜清單!對於陣列中的每一筆食譜資料,顯示它的名稱作為 <h2>,並在 <ul> 中列出其食材。

App.js:

import { recipes } from "./data.js";

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
    </div>
  );
}

data.js:

export const recipes = [
  {
    id: "greek-salad",
    name: "Greek Salad",
    ingredients: ["tomatoes", "cucumber", "onion", "olives", "feta"],
  },
  {
    id: "hawaiian-pizza",
    name: "Hawaiian Pizza",
    ingredients: ["pizza crust", "pizza sauce", "mozzarella", "ham", "pineapple"],
  },
  {
    id: "hummus",
    name: "Hummus",
    ingredients: ["chickpeas", "olive oil", "garlic cloves", "lemon", "tahini"],
  },
];

挑戰 3 / 4:拆出一個清單項目的元件

這個 RecipeList 元件中有兩層 map。為了讓程式更簡潔,請把它拆出一個 Recipe 元件,並接受 idnameingredients 作為 props。請思考:你應該將外層的 key 放在哪裡?為什麼?

import { recipes } from "./data.js";

export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map((recipe) => (
        <div key={recipe.id}>
          <h2>{recipe.name}</h2>
          <ul>
            {recipe.ingredients.map((ingredient) => (
              <li key={ingredient}>{ingredient}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

data.js:

export const recipes = [
  {
    id: "greek-salad",
    name: "Greek Salad",
    ingredients: ["tomatoes", "cucumber", "onion", "olives", "feta"],
  },
  {
    id: "hawaiian-pizza",
    name: "Hawaiian Pizza",
    ingredients: ["pizza crust", "pizza sauce", "mozzarella", "ham", "pineapple"],
  },
  {
    id: "hummus",
    name: "Hummus",
    ingredients: ["chickpeas", "olive oil", "garlic cloves", "lemon", "tahini"],
  },
];

挑戰 4 / 4:帶分隔線的清單

這個範例會顯示橘志坂北枝(Tachibana Hokushi)的一首知名俳句,每一行包在一個 <p> 標籤中。你的任務是要在每段 <p> 中間插入一個 <hr /> 分隔線。最後的 HTML 結構應如下所示:

<article>
  <p>I write, erase, rewrite</p>
  <hr />
  <p>Erase again, and then</p>
  <hr />
  <p>A poppy blooms.</p>
</article>

這首俳句只有三行,但你的解法應該可以處理任意行數。請注意:<hr /> 只能出現在 <p> 元素之間,不要出現在開頭或結尾!

App.js:

const poem = {
  lines: ["I write, erase, rewrite", "Erase again, and then", "A poppy blooms."],
};

export default function Poem() {
  return (
    <article>
      {poem.lines.map((line, index) => (
        <p key={index}>{line}</p>
      ))}
    </article>
  );
}

(這是一個少見的情況,使用陣列索引作為 key 是可以接受的,因為詩句的順序永遠不會變動。)

結語

這篇文章是筆者在學習 React 時,閱讀 React 官方文件 - Rendering Lists 所做的翻譯筆記,希望能幫助更多的人學習 React。

這篇文章主要介紹了如何使用 JavaScript 的 map()filter() 方法來渲染清單,並且強調了在渲染清單時使用 key 的重要性。這些概念對於 React 開發者來說是非常基礎且重要的,希望能幫助你更深入地理解 React 的運作方式。

本文參考資料

https://react.dev/learn/rendering-lists

liz_avatar

Liz

Software Engineer

A dad joke lover and meme collector. A bit socially anxious. Consistently testing as ISTP in the MBTI for several years. Favorite activity is spending the whole day playing video games on the couch with the best friends.

Check more from this author

Share to

Back