React 列表與 Key (Lists and Keys)

在 React 中,我們經常需要將陣列資料渲染成一系列的元素。這時會使用 JavaScript 的 map() 方法來將資料轉換成 React 元素。

基本列表渲染

使用 map() 將陣列轉換成元素列表:

function NumberList() {
  const numbers = [1, 2, 3, 4, 5]

  return (
    <ul>
      {numbers.map((number) => (
        <li key={number}>{number}</li>
      ))}
    </ul>
  )
}

這段程式碼會渲染出:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>

Key 的重要性

你可能注意到上面例子中有個 key 屬性。Key 是 React 用來識別哪些元素有變動(新增、刪除、重新排序)的重要依據。

如果你沒有提供 key,React 會在開發環境中顯示警告:

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

Key 的作用

Key 幫助 React 識別哪些項目有變動。當列表重新渲染時,React 會比較新舊列表的 key:

  • 有新的 key 出現 → 新增元素
  • 舊的 key 消失 → 刪除元素
  • key 相同但位置改變 → 移動元素
  • key 相同且位置相同 → 更新元素(如果內容有變)

選擇正確的 Key

✅ 好的 Key:唯一且穩定的識別碼

最好的 key 是資料中唯一且穩定的識別碼,例如資料庫的 ID:

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

⚠️ 避免使用陣列索引作為 Key

使用陣列索引(index)作為 key 是常見的錯誤做法:

// ⚠️ 不推薦:使用 index 作為 key
{
  items.map((item, index) => <li key={index}>{item.name}</li>)
}

為什麼不好?當列表項目順序改變(如排序、新增、刪除)時,index 會改變,React 可能會錯誤地重用元素,導致:

  • 效能問題(不必要的重新渲染)
  • 表單輸入值混亂
  • 動畫異常

什麼時候可以用 index?

只有在以下情況可以安全地使用 index 作為 key:

  1. 列表項目是靜態的,不會改變
  2. 列表項目不會重新排序
  3. 列表項目沒有穩定的 ID
// ✅ 靜態列表可以用 index
const menuItems = ['首頁', '關於', '聯絡']

function Menu() {
  return (
    <nav>
      {menuItems.map((item, index) => (
        <a key={index} href="#">
          {item}
        </a>
      ))}
    </nav>
  )
}

產生唯一 Key

如果資料沒有唯一 ID,你可以在新增資料時產生一個:

import { useState } from 'react'

function TodoApp() {
  const [todos, setTodos] = useState([])
  const [inputValue, setInputValue] = useState('')

  function addTodo() {
    const newTodo = {
      // 使用時間戳記或 crypto.randomUUID() 產生唯一 ID
      id: crypto.randomUUID(),
      text: inputValue,
      completed: false,
    }
    setTodos([...todos, newTodo])
    setInputValue('')
  }

  return (
    <div>
      <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
      <button onClick={addTodo}>新增</button>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </div>
  )
}

Key 只在兄弟元素間需要唯一

Key 不需要全域唯一,只需要在同一個陣列的兄弟元素之間唯一即可:

function App() {
  const posts = [
    { id: 1, title: '文章 1' },
    { id: 2, title: '文章 2' },
  ]
  const comments = [
    { id: 1, text: '留言 1' }, // 這個 id: 1 和上面的 id: 1 不會衝突
    { id: 2, text: '留言 2' },
  ]

  return (
    <div>
      {/* posts 列表 */}
      {posts.map((post) => (
        <article key={post.id}>{post.title}</article>
      ))}

      {/* comments 列表 - key 可以和 posts 重複 */}
      {comments.map((comment) => (
        <p key={comment.id}>{comment.text}</p>
      ))}
    </div>
  )
}

Key 不會傳遞給元件

Key 是 React 內部使用的,不會傳遞給子元件。如果你需要在子元件中使用相同的值,請用另一個 prop 名稱傳遞:

// ❌ 在 Post 元件中無法存取 key
{
  posts.map((post) => <Post key={post.id} />)
}

// ✅ 需要在元件中使用 id,就另外傳遞
{
  posts.map((post) => <Post key={post.id} id={post.id} title={post.title} />)
}

渲染巢狀列表

處理巢狀資料結構時:

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

// 使用
const data = [
  {
    id: 1,
    name: '水果',
    items: [
      { id: 101, name: '蘋果' },
      { id: 102, name: '香蕉' },
    ],
  },
  {
    id: 2,
    name: '蔬菜',
    items: [
      { id: 201, name: '紅蘿蔔' },
      { id: 202, name: '青椒' },
    ],
  },
]

;<CategoryList categories={data} />

使用 Fragment 包裝多個元素

當每個列表項目需要渲染多個元素時,可以使用 Fragment。如果需要 key,必須使用完整的 Fragment 語法:

import { Fragment } from 'react'

function Glossary({ items }) {
  return (
    <dl>
      {items.map((item) => (
        // 使用 Fragment 避免額外的 DOM 節點
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.definition}</dd>
        </Fragment>
      ))}
    </dl>
  )
}

實際範例:可排序的列表

import { useState } from 'react'

function SortableList() {
  const [items, setItems] = useState([
    { id: 1, name: '蘋果', price: 30 },
    { id: 2, name: '香蕉', price: 20 },
    { id: 3, name: '橘子', price: 25 },
  ])
  const [sortBy, setSortBy] = useState('name')

  const sortedItems = [...items].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name)
    }
    return a.price - b.price
  })

  function deleteItem(id) {
    setItems(items.filter((item) => item.id !== id))
  }

  return (
    <div>
      <div>
        排序:
        <button onClick={() => setSortBy('name')}>名稱</button>
        <button onClick={() => setSortBy('price')}>價格</button>
      </div>

      <ul>
        {sortedItems.map((item) => (
          <li key={item.id}>
            {item.name} - ${item.price}
            <button onClick={() => deleteItem(item.id)}>刪除</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

在這個例子中,即使項目順序因排序而改變,因為使用了穩定的 id 作為 key,React 能正確追蹤每個項目。