Trích xuất State Logic vào một Reducer
Những component có nhiều cập nhật state trải rộng khắp nhiều event handler có thể trở nên phức tạp quá mức. Trong những trường hợp này, bạn có thể hợp nhất tất cả logic cập nhật state bên ngoài component của bạn vào một function duy nhất, được gọi là reducer.
Bạn sẽ được học
- Function reducer là gì
- Cách refactor từ
useState
sanguseReducer
- Khi nào nên sử dụng reducer
- Cách viết reducer tốt
Hợp nhất logic state với reducer
Khi những component của bạn phát triển theo độ phức tạp, có thể trở nên khó khăn hơn để nhìn thoáng qua tất cả những cách khác nhau mà state của component được cập nhật. Ví dụ, component TaskApp
bên dưới giữ một mảng tasks
trong state và sử dụng ba event handler khác nhau để thêm, xóa và chỉnh sửa task:
import { useState } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, setTasks] = useState(initialTasks); function handleAddTask(text) { setTasks([ ...tasks, { id: nextId++, text: text, done: false, }, ]); } function handleChangeTask(task) { setTasks( tasks.map((t) => { if (t.id === task.id) { return task; } else { return t; } }) ); } function handleDeleteTask(taskId) { setTasks(tasks.filter((t) => t.id !== taskId)); } return ( <> <h1>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Visit Kafka Museum', done: true}, {id: 1, text: 'Watch a puppet show', done: false}, {id: 2, text: 'Lennon Wall pic', done: false}, ];
Mỗi event handler của nó gọi setTasks
để cập nhật state. Khi component này phát triển, cũng như số lượng logic state rải rác khắp nó. Để giảm độ phức tạp này và giữ tất cả logic của bạn ở một nơi dễ truy cập, bạn có thể di chuyển logic state đó thành một function duy nhất bên ngoài component của bạn, được gọi là “reducer”.
Reducer là một cách khác để xử lý state. Bạn có thể di chuyển từ useState
sang useReducer
trong ba bước:
- Di chuyển từ việc setting state sang dispatching action.
- Viết một function reducer.
- Sử dụng reducer từ component của bạn.
Bước 1: Di chuyển từ setting state sang dispatching action
Những event handler hiện tại của bạn chỉ định phải làm gì bằng cách setting state:
function handleAddTask(text) {
setTasks([
...tasks,
{
id: nextId++,
text: text,
done: false,
},
]);
}
function handleChangeTask(task) {
setTasks(
tasks.map((t) => {
if (t.id === task.id) {
return task;
} else {
return t;
}
})
);
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter((t) => t.id !== taskId));
}
Loại bỏ tất cả logic setting state. Những gì bạn còn lại là ba event handler:
handleAddTask(text)
được gọi khi người dùng nhấn “Add”.handleChangeTask(task)
được gọi khi người dùng toggle một task hoặc nhấn “Save”.handleDeleteTask(taskId)
được gọi khi người dùng nhấn “Delete”.
Quản lý state với reducer hơi khác so với việc setting state trực tiếp. Thay vì nói với React “phải làm gì” bằng cách setting state, bạn chỉ định “người dùng vừa làm gì” bằng cách dispatching “action” từ những event handler của bạn. (Logic cập nhật state sẽ tồn tại ở nơi khác!) Vì vậy thay vì “setting tasks
” thông qua một event handler, bạn đang dispatching một action “added/changed/deleted a task”. Điều này mô tả ý định của người dùng một cách rõ ràng hơn.
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,
});
}
Object bạn truyền cho dispatch
được gọi là “action”:
function handleDeleteTask(taskId) {
dispatch(
// "action" object:
{
type: 'deleted',
id: taskId,
}
);
}
Đó là một object JavaScript thông thường. Bạn quyết định đặt gì vào đó, nhưng nói chung nó nên chứa thông tin tối thiểu về điều gì đã xảy ra. (Bạn sẽ thêm chính function dispatch
ở bước sau.)
Bước 2: Viết một function reducer
Function reducer là nơi bạn sẽ đặt logic state của mình. Nó nhận hai tham số, state hiện tại và object action, và nó trả về state tiếp theo:
function yourReducer(state, action) {
// return next state for React to set
}
React sẽ set state thành những gì bạn return từ reducer.
Để di chuyển logic setting state từ những event handler của bạn sang một function reducer trong ví dụ này, bạn sẽ:
- Khai báo state hiện tại (
tasks
) như tham số đầu tiên. - Khai báo object
action
như tham số thứ hai. - Return state tiếp theo từ reducer (mà React sẽ set state thành).
Đây là tất cả logic setting state được di chuyển sang một function reducer:
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [
...tasks,
{
id: action.id,
text: action.text,
done: false,
},
];
} else if (action.type === 'changed') {
return tasks.map((t) => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter((t) => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}
Bởi vì function reducer nhận state (tasks
) như một tham số, bạn có thể khai báo nó bên ngoài component của bạn. Điều này giảm mức độ thụt lề và có thể làm cho code của bạn dễ đọc hơn.
Tìm hiểu sâu
Mặc dù reducer có thể “giảm” số lượng code bên trong component của bạn, thực tế chúng được đặt tên theo phép toán reduce()
mà bạn có thể thực hiện trên mảng.
Phép toán reduce()
cho phép bạn lấy một mảng và “tích lũy” một giá trị duy nhất từ nhiều giá trị:
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
(result, number) => result + number
); // 1 + 2 + 3 + 4 + 5
Function bạn truyền cho reduce
được gọi là “reducer”. Nó nhận kết quả hiện tại và phần tử hiện tại, sau đó nó trả về kết quả tiếp theo. React reducer là một ví dụ của cùng một ý tưởng: chúng nhận state hiện tại và action, và trả về state tiếp theo. Theo cách này, chúng tích lũy những action theo thời gian thành state.
Bạn thậm chí có thể sử dụng method reduce()
với một initialState
và một mảng actions
để tính toán state cuối cùng bằng cách truyền function reducer của bạn vào nó:
import tasksReducer from './tasksReducer.js'; let initialState = []; let actions = [ {type: 'added', id: 1, text: 'Visit Kafka Museum'}, {type: 'added', id: 2, text: 'Watch a puppet show'}, {type: 'deleted', id: 1}, {type: 'added', id: 3, text: 'Lennon Wall pic'}, ]; let finalState = actions.reduce(tasksReducer, initialState); const output = document.getElementById('output'); output.textContent = JSON.stringify(finalState, null, 2);
Có thể bạn sẽ không cần phải tự làm điều này, nhưng đây là tương tự với những gì React làm!
Bước 3: Sử dụng reducer từ component của bạn
Cuối cùng, bạn cần kết nối tasksReducer
với component của bạn. Import Hook useReducer
từ React:
import { useReducer } from 'react';
Sau đó bạn có thể thay thế useState
:
const [tasks, setTasks] = useState(initialTasks);
bằng useReducer
như thế này:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Hook useReducer
tương tự như useState
—bạn phải truyền cho nó một initial state và nó trả về một stateful value và một cách để set state (trong trường hợp này, function dispatch). Nhưng nó hơi khác một chút.
Hook useReducer
nhận hai tham số:
- Một function reducer
- Một initial state
Và nó trả về:
- Một stateful value
- Một function dispatch (để “dispatch” những action của người dùng tới reducer)
Bây giờ nó đã được kết nối đầy đủ! Ở đây, reducer được khai báo ở cuối file component:
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>Prague itinerary</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: 'Visit Kafka Museum', done: true}, {id: 1, text: 'Watch a puppet show', done: false}, {id: 2, text: 'Lennon Wall pic', done: false}, ];
Nếu bạn muốn, bạn thậm chí có thể di chuyển reducer sang một file khác:
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import tasksReducer from './tasksReducer.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>Prague itinerary</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } let nextId = 3; const initialTasks = [ {id: 0, text: 'Visit Kafka Museum', done: true}, {id: 1, text: 'Watch a puppet show', done: false}, {id: 2, text: 'Lennon Wall pic', done: false}, ];
Component logic có thể dễ đọc hơn khi bạn tách biệt các mối quan tâm như thế này. Bây giờ những event handler chỉ chỉ định điều gì đã xảy ra bằng cách dispatching action, và function reducer xác định cách state cập nhật để phản hồi chúng.
So sánh useState
và useReducer
Reducer không phải là không có nhược điểm! Đây là một vài cách bạn có thể so sánh chúng:
- Kích thước code: Nói chung, với
useState
bạn phải viết ít code hơn từ đầu. VớiuseReducer
, bạn phải viết cả function reducer và dispatch action. Tuy nhiên,useReducer
có thể giúp cắt giảm code nếu nhiều event handler chỉnh sửa state theo cách tương tự. - Khả năng đọc:
useState
rất dễ đọc khi những cập nhật state đơn giản. Khi chúng trở nên phức tạp hơn, chúng có thể làm phình to code component của bạn và làm cho nó khó để quét qua. Trong trường hợp này,useReducer
cho phép bạn tách biệt một cách sạch sẽ cách thức của logic cập nhật khỏi điều gì đã xảy ra của những event handler. - Debugging: Khi bạn có bug với
useState
, có thể khó để nói ở đâu state được set không chính xác, và tại sao. VớiuseReducer
, bạn có thể thêm console log vào reducer của mình để thấy mọi cập nhật state, và tại sao nó xảy ra (doaction
nào). Nếu mỗiaction
đúng, bạn sẽ biết rằng lỗi nằm trong chính logic reducer. Tuy nhiên, bạn phải bước qua nhiều code hơn so vớiuseState
. - Testing: Reducer là một pure function không phụ thuộc vào component của bạn. Điều này có nghĩa là bạn có thể export và test nó riêng biệt trong cô lập. Mặc dù nói chung tốt nhất là test component trong môi trường thực tế hơn, đối với logic cập nhật state phức tạp, có thể hữu ích để khẳng định rằng reducer của bạn trả về một state cụ thể cho một initial state và action cụ thể.
- Sở thích cá nhân: Một số người thích reducer, những người khác thì không. Điều đó ổn. Đó là vấn đề sở thích. Bạn luôn có thể chuyển đổi giữa
useState
vàuseReducer
qua lại: chúng tương đương!
Chúng tôi khuyên sử dụng reducer nếu bạn thường gặp bug do cập nhật state không chính xác trong một số component, và muốn giới thiệu thêm cấu trúc vào code của nó. Bạn không phải sử dụng reducer cho mọi thứ: cứ thoải mái kết hợp và phối hợp! Bạn thậm chí có thể useState
và useReducer
trong cùng một component.
Viết reducer tốt
Hãy ghi nhớ hai mẹo này khi viết reducer:
- Reducer phải là pure. Tương tự như state updater function, reducer chạy trong quá trình render! (Action được xếp hàng cho đến lần render tiếp theo.) Điều này có nghĩa là reducer phải là pure—cùng input luôn dẫn đến cùng output. Chúng không nên gửi request, lên lịch timeout, hoặc thực hiện bất kỳ side effect nào (phép toán ảnh hưởng đến những thứ bên ngoài component). Chúng nên cập nhật object và mảng mà không có mutation.
- Mỗi action mô tả một tương tác người dùng duy nhất, ngay cả khi điều đó dẫn đến nhiều thay đổi trong dữ liệu. Ví dụ, nếu người dùng nhấn “Reset” trên một form với năm field được quản lý bởi reducer, việc dispatch một action
reset_form
có ý nghĩa hơn so với năm actionset_field
riêng biệt. Nếu bạn log mọi action trong reducer, log đó nên đủ rõ ràng để bạn tái tạo những tương tác hoặc phản hồi nào đã xảy ra theo thứ tự nào. Điều này giúp với debugging!
Viết reducer ngắn gọn với Immer
Giống như với cập nhật object và mảng trong state thông thường, bạn có thể sử dụng thư viện Immer để làm cho reducer ngắn gọn hơn. Ở đây, useImmerReducer
cho phép bạn mutate state với push
hoặc gán arr[i] =
:
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
Reducer phải là pure, vì vậy chúng không nên mutate state. Nhưng Immer cung cấp cho bạn một object draft
đặc biệt mà an toàn để mutate. Bên dưới, Immer sẽ tạo một bản copy của state của bạn với những thay đổi bạn đã thực hiện trên draft
. Đây là lý do tại sao những reducer được quản lý bởi useImmerReducer
có thể mutate tham số đầu tiên của chúng và không cần return state.
Tóm tắt
- Để chuyển đổi từ
useState
sanguseReducer
:- Dispatch action từ những event handler.
- Viết một function reducer trả về state tiếp theo cho một state và action đã cho.
- Thay thế
useState
bằnguseReducer
.
- Reducer yêu cầu bạn viết nhiều code hơn một chút, nhưng chúng giúp với debugging và testing.
- Reducer phải là pure.
- Mỗi action mô tả một tương tác người dùng duy nhất.
- Sử dụng Immer nếu bạn muốn viết reducer theo kiểu mutating.
Challenge 1 of 4: Dispatch action từ event handler
Hiện tại, những event handler trong ContactList.js
và Chat.js
có comment // TODO
. Đây là lý do tại sao việc gõ vào input không hoạt động, và nhấp vào những button không thay đổi người nhận được chọn.
Thay thế hai // TODO
này bằng code để dispatch
những action tương ứng. Để thấy hình dạng mong đợi và loại của những action, kiểm tra reducer trong messengerReducer.js
. Reducer đã được viết sẵn nên bạn sẽ không cần thay đổi nó. Bạn chỉ cần dispatch những action trong ContactList.js
và Chat.js
.
import { useReducer } from 'react'; import Chat from './Chat.js'; import ContactList from './ContactList.js'; import { initialState, messengerReducer } from './messengerReducer'; export default function Messenger() { const [state, dispatch] = useReducer(messengerReducer, initialState); const message = state.message; const contact = contacts.find((c) => c.id === state.selectedId); return ( <div> <ContactList contacts={contacts} selectedId={state.selectedId} dispatch={dispatch} /> <Chat key={contact.id} message={message} contact={contact} dispatch={dispatch} /> </div> ); } const contacts = [ {id: 0, name: 'Taylor', email: 'taylor@mail.com'}, {id: 1, name: 'Alice', email: 'alice@mail.com'}, {id: 2, name: 'Bob', email: 'bob@mail.com'}, ];