Mở rộng quy mô với Reducer và Context
Reducer cho phép bạn hợp nhất logic cập nhật state của component. Context cho phép bạn truyền thông tin xuống sâu tới các component khác. Bạn có thể kết hợp reducer và context lại với nhau để quản lý state của một màn hình phức tạp.
Bạn sẽ được học
- Cách kết hợp reducer với context
- Cách tránh truyền state và dispatch qua props
- Cách giữ logic context và state trong một file riêng biệt
Kết hợp reducer với context
Trong ví dụ này từ phần giới thiệu về reducer, state được quản lý bởi một reducer. Function reducer chứa tất cả logic cập nhật state và được khai báo ở cuối file này:
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 } ];
Một reducer giúp các event handler ngắn gọn và súc tích. Tuy nhiên, khi ứng dụng của bạn phát triển, bạn có thể gặp phải một khó khăn khác. Hiện tại, tasks
state và function dispatch
chỉ có sẵn trong component cấp cao nhất TaskApp
. Để cho phép các component khác đọc danh sách task hoặc thay đổi nó, bạn phải một cách rõ ràng truyền xuống state hiện tại và các event handler thay đổi nó dưới dạng props.
Ví dụ, TaskApp
truyền danh sách task và các event handler tới TaskList
:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
Và TaskList
truyền các event handler tới Task
:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
Trong một ví dụ nhỏ như thế này, điều này hoạt động tốt, nhưng nếu bạn có hàng chục hoặc hàng trăm component ở giữa, việc truyền xuống tất cả state và function có thể khá bực bội!
Đây là lý do tại sao, như một giải pháp thay thế cho việc truyền chúng qua props, bạn có thể muốn đặt cả tasks
state và function dispatch
vào context. Bằng cách này, bất kỳ component nào bên dưới TaskApp
trong cây có thể đọc task và dispatch action mà không cần “prop drilling” lặp đi lặp lại.
Đây là cách bạn có thể kết hợp reducer với context:
- Tạo context.
- Đặt state và dispatch vào context.
- Use context ở bất kỳ đâu trong cây.
Bước 1: Tạo context
Hook useReducer
trả về tasks
hiện tại và function dispatch
cho phép bạn cập nhật chúng:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Để truyền chúng xuống cây, bạn sẽ tạo hai context riêng biệt:
TasksContext
cung cấp danh sách task hiện tại.TasksDispatchContext
cung cấp function cho phép các component dispatch action.
Export chúng từ một file riêng biệt để sau này bạn có thể import chúng từ các file khác:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Ở đây, bạn đang truyền null
làm giá trị mặc định cho cả hai context. Các giá trị thực tế sẽ được cung cấp bởi component TaskApp
.
Bước 2: Đặt state và dispatch vào context
Bây giờ bạn có thể import cả hai context trong component TaskApp
của mình. Lấy tasks
và dispatch
được trả về bởi useReducer()
và cung cấp chúng cho toàn bộ cây bên dưới:
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
...
</TasksDispatchContext>
</TasksContext>
);
}
Hiện tại, bạn truyền thông tin cả qua props và trong 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 value={tasks}> <TasksDispatchContext value={dispatch}> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext> </TasksContext> ); } 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 } ];
Trong bước tiếp theo, bạn sẽ loại bỏ việc truyền props.
Bước 3: Use context ở bất kỳ đâu trong cây
Bây giờ bạn không cần truyền danh sách task hoặc các event handler xuống cây:
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext>
</TasksContext>
Thay vào đó, bất kỳ component nào cần danh sách task có thể đọc nó từ TaskContext
:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
Để cập nhật danh sách task, bất kỳ component nào có thể đọc function dispatch
từ context và gọi nó:
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>
// ...
Component TaskApp
không truyền bất kỳ event handler nào xuống, và TaskList
cũng không truyền bất kỳ event handler nào tới component Task
. Mỗi component đọc context mà nó cần:
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> ); }
State vẫn “sống” trong component cấp cao nhất TaskApp
, được quản lý bằng useReducer
. Nhưng tasks
và dispatch
của nó giờ đây có sẵn cho mọi component bên dưới trong cây bằng cách import và use những context này.
Chuyển tất cả wiring vào một file duy nhất
Bạn không bắt buộc phải làm điều này, nhưng bạn có thể làm gọn gàng hơn các component bằng cách chuyển cả reducer và context vào một file duy nhất. Hiện tại, TasksContext.js
chỉ chứa hai khai báo context:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
File này sắp trở nên đông đúc! Bạn sẽ chuyển reducer vào cùng file đó. Sau đó bạn sẽ khai báo một component TasksProvider
mới trong cùng file. Component này sẽ kết nối tất cả các phần lại với nhau:
- Nó sẽ quản lý state bằng reducer.
- Nó sẽ cung cấp cả hai context cho các component bên dưới.
- Nó sẽ nhận
children
làm prop để bạn có thể truyền JSX vào nó.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
Điều này loại bỏ tất cả sự phức tạp và wiring khỏi component TaskApp
của bạn:
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> ); }
Bạn cũng có thể export những function use context từ TasksContext.js
:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Khi một component cần đọc context, nó có thể làm điều đó thông qua những function này:
const tasks = useTasks();
const dispatch = useTasksDispatch();
Điều này không thay đổi hành vi theo bất kỳ cách nào, nhưng nó cho phép bạn sau này tách những context này thêm hoặc thêm một số logic vào những function này. Bây giờ tất cả wiring context và reducer đều nằm trong TasksContext.js
. Điều này giữ cho các component sạch sẽ và gọn gàng, tập trung vào những gì chúng hiển thị hơn là nơi chúng lấy dữ liệu:
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> ); }
Bạn có thể nghĩ về TasksProvider
như một phần của màn hình biết cách xử lý task, useTasks
như một cách để đọc chúng, và useTasksDispatch
như một cách để cập nhật chúng từ bất kỳ component nào bên dưới trong cây.
Khi ứng dụng của bạn phát triển, bạn có thể có nhiều cặp context-reducer như thế này. Đây là một cách mạnh mẽ để mở rộng quy mô ứng dụng của bạn và nâng state lên mà không cần quá nhiều công việc bất cứ khi nào bạn muốn truy cập dữ liệu sâu trong cây.
Tóm tắt
- Bạn có thể kết hợp reducer với context để cho phép bất kỳ component nào đọc và cập nhật state phía trên nó.
- Để cung cấp state và function dispatch cho các component bên dưới:
- Tạo hai context (cho state và cho dispatch function).
- Cung cấp cả hai context từ component use reducer.
- Use context từ các component cần đọc chúng.
- Bạn có thể làm gọn gàng hơn các component bằng cách chuyển tất cả wiring vào một file.
- Bạn có thể export một component như
TasksProvider
để cung cấp context. - Bạn cũng có thể export các Custom Hook như
useTasks
vàuseTasksDispatch
để đọc nó.
- Bạn có thể export một component như
- Bạn có thể có nhiều cặp context-reducer như thế này trong ứng dụng của mình.