# 資料流(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、跨頁跨模組靠狀態庫與資料抓取工具**。