React 組件狀態傳遞
## React 如何管理多個組件間的狀態傳遞
React 的核心觀念是「**單向資料流(One-way Data Flow)**」:
1. 狀態通常由共同父組件管理。
2. 父組件透過 `props` 將資料傳給子組件。
3. 子組件不能直接修改父組件的狀態。
4. 子組件可以呼叫父組件傳入的 callback,通知父組件更新狀態。
5. 狀態更新後,React 重新渲染相關組件。
### 常見做法
- **父傳子**:使用 `props`
- **子傳父**:父組件傳入事件函式,例如 `onAdd`
- **兄弟組件互傳**:將共享狀態提升到共同父組件,稱為 **Lifting State Up**
- **跨多層組件傳遞**:可使用 `Context`
- **大型應用程式**:可使用 Redux、Zustand、Jotai 等狀態管理工具
---
## 整合範例:待辦事項
以下範例包含:
- `App`:管理所有待辦事項狀態
- `TodoForm`:新增待辦事項
- `TodoList`:顯示待辦事項
- `TodoItem`:切換完成狀態或刪除
- `TodoSummary`:顯示統計資訊
```jsx
import { useState } from "react";
function App() {
const [todos, setTodos] = useState([]);
const [nextId, setNextId] = useState(1);
function addTodo(title) {
const newTodo = {
id: nextId,
title,
completed: false,
};
setTodos((currentTodos) => [...currentTodos, newTodo]);
setNextId((id) => id + 1);
}
function toggleTodo(id) {
setTodos((currentTodos) =>
currentTodos.map((todo) =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
);
}
function deleteTodo(id) {
setTodos((currentTodos) =>
currentTodos.filter((todo) => todo.id !== id)
);
}
return (
<div>
<h1>待辦事項</h1>
<TodoForm onAdd={addTodo} />
<TodoSummary todos={todos} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
function TodoForm({ onAdd }) {
const [title, setTitle] = useState("");
function handleSubmit(event) {
event.preventDefault();
const trimmedTitle = title.trim();
if (!trimmedTitle) return;
onAdd(trimmedTitle);
setTitle("");
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="輸入待辦事項"
/>
<button type="submit">新增</button>
</form>
);
}
function TodoList({ todos, onToggle, onDelete }) {
if (todos.length === 0) {
return <p>目前沒有待辦事項</p>;
}
return (
<ul>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
function TodoItem({ todo, onToggle, onDelete }) {
return (
<li>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span
style={{
textDecoration: todo.completed
? "line-through"
: "none",
}}
>
{todo.title}
</span>
</label>
<button onClick={() => onDelete(todo.id)}>
刪除
</button>
</li>
);
}
function TodoSummary({ todos }) {
const completedCount = todos.filter(
(todo) => todo.completed
).length;
return (
<p>
共 {todos.length} 項,已完成 {completedCount} 項
</p>
);
}
export default App;
```
## 單向資料流分析
以上範例的資料流如下:
```text
App
├─ todos ────────────────> TodoSummary
└─ todos ────────────────> TodoList
└─ todo ─────> TodoItem
TodoForm ── onAdd(title) ──> App
TodoItem ── onToggle(id) ──> App
TodoItem ── onDelete(id) ──> App
```
### 1. `App` 管理共享狀態
```jsx
const [todos, setTodos] = useState([]);
```
因為 `TodoList`、`TodoSummary` 都需要使用 `todos`,所以狀態放在它們的共同父組件 `App` 中。
### 2. 父組件將資料傳給子組件
```jsx
<TodoList todos={todos} />
<TodoSummary todos={todos} />
```
子組件透過 `props` 接收資料:
```jsx
function TodoSummary({ todos }) {
// 使用 todos
}
```
### 3. 子組件透過 callback 通知父組件
```jsx
<TodoForm onAdd={addTodo} />
```
`TodoForm` 收到 `onAdd` 後,在表單送出時呼叫:
```jsx
onAdd(trimmedTitle);
```
真正修改 `todos` 的仍然是 `App`:
```jsx
function addTodo(title) {
setTodos((currentTodos) => [
...currentTodos,
{
id: nextId,
title,
completed: false,
},
]);
}
```
### 4. 狀態更新後資料再次向下流動
當 `App` 更新 `todos` 後:
```text
App 更新 todos
↓
TodoSummary 顯示新的統計
TodoList 顯示新的清單
TodoItem 顯示新的完成狀態
```
因此,React 中的狀態傳遞通常是:
```text
父組件:資料與狀態
↓ props
子組件:顯示資料
↑ callback
父組件:處理更新
```
這種方式能讓資料來源清楚、狀態容易追蹤,也能避免不同組件各自維護相同資料而造成不一致。
相關學習地圖、教學課程
F2E 網站前端工程