---
# System prepended metadata

title: 資料流（Data Flow）——從入門到實務

---


# 資料流（Data Flow）——從入門到實務

## 單向資料流（Top‑down）為核心

* **資料只往下流**：父元件 → 子元件（透過 `props`）。
* **子元件不直接改 `props`**：要改資料，由**回呼**把意圖往上拋給父元件處理（父元件持有狀態並更新）。
* **優點**：可預測、可測試、思路清楚。

📌 **口訣**：**狀態靠近使用它的地方**（State Colocation）。需要多個子元件共享時，再把狀態往上「提升（Lifting State Up）」到它們的最近共同父層。

<br/>

## 父 → 子：以 `props` 傳入資料與行為

**Btn.jsx**（簡化 JS 版）

```jsx
const Btn = ({ color, onClick }) => (
  <button
    style={{ backgroundColor: color, padding: '20px', color: 'white' }}
    onClick={onClick}
  >
    Click me {color}
  </button>
);
export default Btn;
```

**App.js**

```jsx
import Btn from './components/Btn';

function App() {
  return (
    <div>
      <Btn color="red" onClick={() => console.log('red')} />
      <Btn color="blue" onClick={() => console.log('blue')} />
    </div>
  );
}
export default App;
```

> **重點**：`props` 可以傳**資料**（`color`）也可以傳**行為**（`onClick`）。

![](https://hackmd.io/_uploads/H1NGzGoKR.png)


<br/>

## 子 → 父：回呼上拋（Callback to Parent）

當子元件需要「觸發修改」時，把事件透過回呼交給父層處理——**資料仍在父層更新**。

**TodoList / TodoItem（精簡示例）**

```jsx
// TodoItem.jsx
const TodoItem = ({ todo, onRemove }) => (
  <article>
    <h2>{todo.title}</h2>
    <p>{todo.description}</p>
    <button onClick={() => onRemove(todo._id)}>Remove</button>
  </article>
);
export default TodoItem;
```

```jsx
// TodoList/index.jsx
import { useState, useCallback } from 'react';
import TodoItem from './TodoItem';

const TodoList = () => {
  const [todoList, setTodoList] = useState([
    { _id: 1, title: 'Todo 1', description: 'Todo 1 description' },
    { _id: 2, title: 'Todo 2', description: 'Todo 2 description' },
  ]);

  const removeTodo = useCallback((id) => {
    setTodoList((list) => list.filter((t) => t._id !== id));
  }, []);

  return (
    <article>
      <h1>TodoList</h1>
      <ul>
        {todoList.map((todo) => (
          <li key={todo._id}>
            <TodoItem todo={todo} onRemove={removeTodo} />
          </li>
        ))}
      </ul>
    </article>
  );
};
export default TodoList;
```

> **最佳實務**：
>
> * `key` 用穩定 ID（`_id`），避免用 `index`。
> * 用 `useCallback` 穩定回呼參考，可搭配 `React.memo` 降低不必要重渲染。

<br/>

## 受控元件（Controlled Components）

把表單輸入綁到 state，**單一資料源**讓 UI 與資料永遠同步。

```jsx
const [keyword, setKeyword] = useState('');
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<ul>
  {todoList
    .filter((t) => t.title.toLowerCase().includes(keyword.toLowerCase()))
    .map((t) => (
      <li key={t.id}>{t.title}</li>
    ))}
</ul>
```

<br/>

## Context：避免層層傳遞（但別濫用）

**什麼時候用？** 當資料需要跨越多層傳遞（如 **目前使用者、主題、語系、權限**），就適合放 Context。

```jsx
import { createContext, useContext, useMemo, useState } from 'react';

const UserContext = createContext(null);

export const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};

export const useUser = () => useContext(UserContext);
```

> **設計要點**：
>
> * 以 `useMemo` 穩定 `value`，降低 Provider 下所有子孫重渲染。
> * 多筆 Context 拆分（如 `ThemeContext`、`AuthContext`），**不要**把所有東西塞一個巨大的 Context。
> * 複雜讀取邏輯可用 **Selector Context**（為 `value` 提供切片），或改用外部狀態管理。

<br/>

## 伺服器資料 vs. 本地狀態：不同工具處理不同問題

* **本地狀態（UI 狀態）**：Modal 開關、輸入欄位值、目前頁碼… → `useState` / `useReducer` + Context。
* **伺服器狀態（Server State）**：來自 API 的可快取資料、同步/重新整理、背景更新、錯誤/載入狀態… → **React Query / SWR** 這類**資料抓取與快取工具**。
* **全域業務狀態（複雜讀寫、多人協作、可預測流程）**：用 **Redux Toolkit** 或 **Zustand/Jotai** 等狀態庫更好維護。

> **實務法則**：
>
> 1. 能就近放的 state，就近放（Colocation）。
> 2. 牽涉 API 快取與同步，優先考慮 React Query。
> 3. 需要跨許多頁面共享、流程複雜、可追蹤性要求高時，考慮全域狀態庫。

---

## 常見錯誤與避坑

* 直接改 state（例如 `state.list.push()`）→ 應回傳**新物件/新陣列**。
* 為了「以後可能會用到」就把所有 state 往上提 → 先靠近用處；真的需要共享再提升。
* Context 裝一切 → 造成整棵樹頻繁重渲染與耦合；拆 Context 或改用專門的狀態工具。
* `key` 用陣列索引 → 在新增/刪除時導致錯位與重用錯誤。

<br/>

## 範例：非同步載入 + 搜尋 + 刪除（整合版）

```jsx
import { useEffect, useMemo, useState, useCallback } from 'react';
import TodoItem from './TodoItem';

const TodoList = () => {
  const [todoList, setTodoList] = useState([]);
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/todos?_limit=50')
      .then((r) => r.json())
      .then((data) => setTodoList(data));
  }, []);

  const deleteTodo = useCallback((id) => {
    setTodoList((list) => list.filter((t) => t.id !== id));
  }, []);

  const filtered = useMemo(() => {
    const k = keyword.trim().toLowerCase();
    return k ? todoList.filter((t) => t.title.toLowerCase().includes(k)) : todoList;
  }, [keyword, todoList]);

  return (
    <div>
      <h1>Todo list</h1>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="搜尋標題" />
      <ul>
        {filtered.map((todo, index) => (
          <TodoItem key={todo.id} index={index} todo={todo} deleteTodo={deleteTodo} />
        ))}
      </ul>
    </div>
  );
};
export default TodoList;
```

```jsx
// TodoItem.jsx
import { useState, memo } from 'react';

const TodoItem = memo(({ todo, index, deleteTodo }) => {
  const { id, title, completed } = todo;
  const [open, setOpen] = useState(false);
  return (
    <li>
      <h2 onClick={() => setOpen((v) => !v)}>
        {index + 1}. {title}
      </h2>
      {open && (
        <>
          <p>{completed ? '完成' : '未完成'}</p>
          <button onClick={() => deleteTodo(id)}>Delete</button>
        </>
      )}
    </li>
  );
});
export default TodoItem;
```
[範例檔案](https://github.com/IffyArt/2025-fullstack-course-forntend/tree/feature/react-todolist)

<br/>

## 小結（速記版）

* **單向資料流**：父傳子用 `props`，子改資料用回呼上拋。
* **狀態放哪裡？** 一律先「就近放」，需要共享再提升；跨層再考慮 Context。
* **不同類型狀態用不同工具**：UI 狀態 → `useState/useReducer`；伺服器資料 → React Query；大型全域狀態 → Redux Toolkit / Zustand。
* **效能**：`key` 用 ID、用 `memo` 與 `useCallback`/`useMemo` 穩定參考。

> 這就是 React 資料流的全景地圖：**自下而上靠回呼、跨層靠 Context、跨頁跨模組靠狀態庫與資料抓取工具**。
