Reducers 讓你可以整合元件的狀態更新邏輯。Context 讓你將資訊深入傳遞給其他元件。你可以將 Reducers 和 Context 結合起來管理複雜螢幕的狀態。
你將學習到
- 如何將 Reducer 與 Context 結合
- 如何避免透過 props 傳遞狀態和 dispatch
- 如何在單獨的檔案中維護 Context 和狀態邏輯
將 Reducer 與 Context 結合
在這個來自 Reducer 簡介 的範例中,狀態是由 Reducer 管理的。Reducer 函式包含所有狀態更新邏輯,並在此檔案的底部宣告
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
Reducer 有助於保持事件處理程式簡潔明瞭。然而,隨著應用程式的增長,你可能會遇到另一個困難。**目前,`tasks` 狀態和 `dispatch` 函式僅在頂層的 `TaskApp` 元件中可用。** 為了讓其他元件讀取任務列表或更改它,你必須明確地**將**目前的狀態和更改它的事件處理程式作為 props 傳遞下去。
例如,`TaskApp` 將任務列表和事件處理程式傳遞給 `TaskList`
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
而 `TaskList` 將事件處理程式傳遞給 `Task`
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
在像這樣的小範例中,這樣做效果很好,但是如果中間有數十或數百個元件,傳遞所有狀態和函式可能會非常令人沮喪!
這就是為什麼,作為透過 props 傳遞它們的替代方案,你可能希望將 `tasks` 狀態和 `dispatch` 函式**放入 Context 中。** **這樣,樹狀結構中 `TaskApp` 下方的任何元件都可以讀取任務並發送動作,而無需重複的「prop drilling」。**
以下是將 Reducer 與 Context 結合的方法
- **建立** Context。
- **將** 狀態和 dispatch 放入 Context 中。
- **在** 樹狀結構中的任何位置使用 Context。
步驟 1:建立 Context
`useReducer` Hook 會返回目前的 `tasks` 和讓你更新它們的 `dispatch` 函式
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
為了將它們傳遞下去,你將**建立**兩個單獨的 Context
- `TasksContext` 提供目前的任務列表。
- `TasksDispatchContext` 提供允許元件發送動作的函式。
將它們從單獨的檔案中匯出,以便稍後可以從其他檔案中匯入它們
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
在這裡,你將 `null` 作為預設值傳遞給兩個 Context。實際值將由 `TaskApp` 元件提供。
步驟 2:將狀態和 dispatch 放入 context 中
現在,您可以在您的 TaskApp
元件中導入兩個 context。使用 useReducer()
返回的 tasks
和 dispatch
,並將它們提供給下方的整個元件樹。
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
目前,您同時透過 props 和 context 傳遞資訊。
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext.Provider> </TasksContext.Provider> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
在下一步中,您將移除 props 傳遞。
步驟 3:在元件樹中的任何位置使用 context
現在您不需要將任務列表或事件處理函式向下傳遞到元件樹了。
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
任何需要任務列表的元件都可以從 TaskContext
中讀取它。
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
要更新任務列表,任何元件都可以從 context 中讀取 dispatch
函式並呼叫它。
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...
TaskApp
元件不會向下傳遞任何事件處理函式,TaskList
也不會將任何事件處理函式傳遞給 Task
元件。每個元件都會讀取它需要的 context。
import { useState, useContext } from 'react'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useContext(TasksDispatchContext); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
狀態仍然「存在」於頂層的 TaskApp
元件中,並使用 useReducer
進行管理。 但是它的 tasks
和 dispatch
現在可以透過導入和使用這些 context,讓樹狀結構下方的每個元件都能使用。
將所有連線移到單個檔案中
您不必這樣做,但您可以將 reducer 和 context 移到單個檔案中,以進一步簡化元件。目前,TasksContext.js
只包含兩個 context 宣告。
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
這個檔案快要塞滿了!您會將 reducer 移到同一個檔案中。然後,您將在同一個檔案中宣告一個新的 TasksProvider
元件。這個元件會將所有部分連結在一起。
- 它將使用 reducer 管理狀態。
- 它將為下方的元件提供兩個 context。
- 它將將
children
作為 prop 接收,以便您可以將 JSX 傳遞給它。
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
這將從您的 TaskApp
元件中移除所有複雜性和連線。
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
您還可以從 TasksContext.js
導出*使用* context 的函式。
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
當元件需要讀取 context 時,它可以透過這些函式來完成。
const tasks = useTasks();
const dispatch = useTasksDispatch();
這不會以任何方式改變行為,但它可以讓您稍後進一步拆分這些 context 或向這些函式新增一些邏輯。現在所有 context 和 reducer 的連線都在 TasksContext.js
中。這讓元件保持乾淨和整潔,專注於它們顯示的內容,而不是它們從哪裡獲取資料:
import { useState } from 'react'; import { useTasks, useTasksDispatch } from './TasksContext.js'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
您可以將 TasksProvider
視為螢幕的一部分,它知道如何處理任務;將 useTasks
視為讀取它們的方式;將 useTasksDispatch
視為從樹狀結構下方的任何元件更新它們的方式。
隨著應用程式的增長,您可能會有許多像這樣的 context-reducer 對。這是一種強大的方法,可以擴展您的應用程式,並在您想要深入存取樹狀結構中的資料時,在不需太多工作的情況下將狀態提升。
回顧
- 您可以將 reducer 與 context 結合,讓任何元件都能讀取和更新其上方的狀態。
- 要將狀態和 dispatch 函式提供給下方的元件:
- 創建兩個 context(一個用於狀態,一個用於 dispatch 函式)。
- 從使用 reducer 的元件提供兩個 context。
- 從需要讀取它們的元件使用任一 context。
- 您可以將所有連線移到一個檔案中,以進一步簡化元件。
- 您可以導出一個像
TasksProvider
這樣的元件來提供 context。 - 您還可以導出像
useTasks
和useTasksDispatch
這樣的自定義 Hooks 來讀取它。
- 您可以導出一個像
- 您的應用程式中可以有許多像這樣的 context-reducer 對。