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:
- 列表項目是靜態的,不會改變
- 列表項目不會重新排序
- 列表項目沒有穩定的 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 能正確追蹤每個項目。