useReducer
useReducer
là một React Hook cho phép bạn thêm một reducer vào component của bạn.
const [state, dispatch] = useReducer(reducer, initialArg, init?)
- Tham khảo
- Cách sử dụng
- Khắc phục sự cố
- Tôi đã gửi một action, nhưng nhật ký cho tôi giá trị trạng thái cũ
- Tôi đã gửi một action, nhưng màn hình không cập nhật
- Một phần trạng thái reducer của tôi trở thành không xác định sau khi gửi
- Toàn bộ trạng thái reducer của tôi trở thành không xác định sau khi gửi
- Tôi gặp lỗi: “Quá nhiều lần kết xuất lại”
- Hàm reducer hoặc hàm khởi tạo của tôi chạy hai lần
Tham khảo
useReducer(reducer, initialArg, init?)
Gọi useReducer
ở cấp cao nhất của component để quản lý trạng thái của nó bằng một reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
Tham số
reducer
: Hàm reducer chỉ định cách trạng thái được cập nhật. Nó phải là thuần túy, nhận trạng thái và action làm đối số và trả về trạng thái tiếp theo. Trạng thái và action có thể thuộc bất kỳ loại nào.initialArg
: Giá trị từ đó trạng thái ban đầu được tính toán. Nó có thể là một giá trị của bất kỳ loại nào. Cách trạng thái ban đầu được tính toán từ nó phụ thuộc vào đối sốinit
tiếp theo.- tùy chọn
init
: Hàm khởi tạo nên trả về trạng thái ban đầu. Nếu nó không được chỉ định, trạng thái ban đầu được đặt thànhinitialArg
. Nếu không, trạng thái ban đầu được đặt thành kết quả của việc gọiinit(initialArg)
.
Trả về
useReducer
trả về một mảng với chính xác hai giá trị:
- Trạng thái hiện tại. Trong quá trình render đầu tiên, nó được đặt thành
init(initialArg)
hoặcinitialArg
(nếu không cóinit
). - Hàm
dispatch
cho phép bạn cập nhật trạng thái thành một giá trị khác và kích hoạt render lại.
Lưu ý
useReducer
là một Hook, vì vậy bạn chỉ có thể gọi nó ở cấp cao nhất của component hoặc Hook của riêng bạn. Bạn không thể gọi nó bên trong các vòng lặp hoặc điều kiện. Nếu bạn cần điều đó, hãy trích xuất một component mới và di chuyển trạng thái vào đó.- Hàm
dispatch
có một định danh ổn định, vì vậy bạn sẽ thường thấy nó bị bỏ qua khỏi các dependencies của Effect, nhưng việc bao gồm nó sẽ không làm cho Effect kích hoạt. Nếu trình kiểm tra lỗi cho phép bạn bỏ qua một dependency mà không có lỗi, thì việc đó là an toàn. Tìm hiểu thêm về việc loại bỏ các dependencies của Effect. - Trong Strict Mode, React sẽ gọi reducer và trình khởi tạo của bạn hai lần để giúp bạn tìm thấy các tạp chất vô tình. Đây là hành vi chỉ dành cho phát triển và không ảnh hưởng đến sản xuất. Nếu reducer và trình khởi tạo của bạn là thuần túy (như chúng phải vậy), điều này sẽ không ảnh hưởng đến logic của bạn. Kết quả từ một trong các lệnh gọi bị bỏ qua.
Hàm dispatch
Hàm dispatch
được trả về bởi useReducer
cho phép bạn cập nhật trạng thái thành một giá trị khác và kích hoạt render lại. Bạn cần chuyển action làm đối số duy nhất cho hàm dispatch
:
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React sẽ đặt trạng thái tiếp theo thành kết quả của việc gọi hàm reducer
mà bạn đã cung cấp với state
hiện tại và action bạn đã chuyển cho dispatch
.
Tham số
action
: Hành động được thực hiện bởi người dùng. Nó có thể là một giá trị của bất kỳ loại nào. Theo quy ước, một action thường là một đối tượng có thuộc tínhtype
xác định nó và, tùy chọn, các thuộc tính khác với thông tin bổ sung.
Trả về
Hàm dispatch
không có giá trị trả về.
Lưu ý
-
Hàm
dispatch
chỉ cập nhật biến trạng thái cho lần render tiếp theo. Nếu bạn đọc biến trạng thái sau khi gọi hàmdispatch
, bạn vẫn sẽ nhận được giá trị cũ đã có trên màn hình trước khi bạn gọi. -
Nếu giá trị mới bạn cung cấp giống hệt với
state
hiện tại, như được xác định bởi so sánhObject.is
, React sẽ bỏ qua việc render lại component và các component con của nó. Đây là một tối ưu hóa. React vẫn có thể cần gọi component của bạn trước khi bỏ qua kết quả, nhưng nó không ảnh hưởng đến mã của bạn. -
React gom các bản cập nhật trạng thái. Nó cập nhật màn hình sau khi tất cả các trình xử lý sự kiện đã chạy và đã gọi các hàm
set
của chúng. Điều này ngăn chặn nhiều lần render lại trong một sự kiện duy nhất. Trong trường hợp hiếm hoi bạn cần buộc React cập nhật màn hình sớm hơn, ví dụ: để truy cập DOM, bạn có thể sử dụngflushSync
.
Cách sử dụng
Thêm một reducer vào một component
Gọi useReducer
ở cấp cao nhất của component để quản lý trạng thái bằng một reducer.
import { useReducer } from 'react';
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
trả về một mảng với chính xác hai mục:
- Trạng thái hiện tại của biến trạng thái này, ban đầu được đặt thành trạng thái ban đầu mà bạn đã cung cấp.
- Hàm
dispatch
cho phép bạn thay đổi nó để đáp ứng với tương tác.
Để cập nhật những gì trên màn hình, hãy gọi dispatch
với một đối tượng đại diện cho những gì người dùng đã làm, được gọi là một action:
function handleClick() {
dispatch({ type: 'incremented_age' });
}
React sẽ chuyển trạng thái hiện tại và action cho hàm reducer của bạn. Reducer của bạn sẽ tính toán và trả về trạng thái tiếp theo. React sẽ lưu trữ trạng thái tiếp theo đó, render component của bạn với nó và cập nhật UI.
import { useReducer } from 'react'; function reducer(state, action) { if (action.type === 'incremented_age') { return { age: state.age + 1 }; } throw Error('Unknown action.'); } export default function Counter() { const [state, dispatch] = useReducer(reducer, { age: 42 }); return ( <> <button onClick={() => { dispatch({ type: 'incremented_age' }) }}> Increment age </button> <p>Hello! You are {state.age}.</p> </> ); }
useReducer
rất giống với useState
, nhưng nó cho phép bạn di chuyển logic cập nhật trạng thái từ các trình xử lý sự kiện vào một hàm duy nhất bên ngoài component của bạn. Đọc thêm về lựa chọn giữa useState
và useReducer
.
Viết hàm reducer
Một hàm reducer được khai báo như thế này:
function reducer(state, action) {
// ...
}
Sau đó, bạn cần điền vào mã sẽ tính toán và trả về trạng thái tiếp theo. Theo quy ước, nó thường được viết dưới dạng một câu lệnh switch
. Đối với mỗi case
trong switch
, hãy tính toán và trả về một số trạng thái tiếp theo.
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
Actions có thể có bất kỳ hình dạng nào. Theo quy ước, người ta thường truyền các đối tượng có thuộc tính type
xác định action. Nó nên bao gồm thông tin cần thiết tối thiểu mà reducer cần để tính toán trạng thái tiếp theo.
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 });
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
// ...
Tên loại action là cục bộ đối với component của bạn. Mỗi action mô tả một tương tác duy nhất, ngay cả khi điều đó dẫn đến nhiều thay đổi trong dữ liệu. Hình dạng của trạng thái là tùy ý, nhưng thông thường nó sẽ là một đối tượng hoặc một mảng.
Đọc trích xuất logic trạng thái vào một reducer để tìm hiểu thêm.
Example 1 of 3: Biểu mẫu (đối tượng)
Trong ví dụ này, reducer quản lý một đối tượng trạng thái với hai trường: name
và age
.
import { useReducer } from 'react'; function reducer(state, action) { switch (action.type) { case 'incremented_age': { return { name: state.name, age: state.age + 1 }; } case 'changed_name': { return { name: action.nextName, age: state.age }; } } throw Error('Unknown action: ' + action.type); } const initialState = { name: 'Taylor', age: 42 }; export default function Form() { const [state, dispatch] = useReducer(reducer, initialState); function handleButtonClick() { dispatch({ type: 'incremented_age' }); } function handleInputChange(e) { dispatch({ type: 'changed_name', nextName: e.target.value }); } return ( <> <input value={state.name} onChange={handleInputChange} /> <button onClick={handleButtonClick}> Increment age </button> <p>Hello, {state.name}. You are {state.age}.</p> </> ); }
Tránh tạo lại trạng thái ban đầu
React lưu trạng thái ban đầu một lần và bỏ qua nó trong các lần kết xuất tiếp theo.
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, createInitialState(username));
// ...
}
Mặc dù kết quả của createInitialState(username)
chỉ được sử dụng cho lần kết xuất ban đầu, nhưng bạn vẫn gọi hàm này trên mỗi lần kết xuất. Điều này có thể gây lãng phí nếu nó tạo ra các mảng lớn hoặc thực hiện các tính toán tốn kém.
Để giải quyết vấn đề này, bạn có thể truyền nó như một hàm khởi tạo cho useReducer
làm đối số thứ ba:
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
Lưu ý rằng bạn đang truyền createInitialState
, là chính hàm, chứ không phải createInitialState()
, là kết quả của việc gọi nó. Bằng cách này, trạng thái ban đầu không bị tạo lại sau khi khởi tạo.
Trong ví dụ trên, createInitialState
nhận một đối số username
. Nếu trình khởi tạo của bạn không cần bất kỳ thông tin nào để tính toán trạng thái ban đầu, bạn có thể truyền null
làm đối số thứ hai cho useReducer
.
Example 1 of 2: Truyền hàm khởi tạo
Ví dụ này truyền hàm khởi tạo, vì vậy hàm createInitialState
chỉ chạy trong quá trình khởi tạo. Nó không chạy khi thành phần kết xuất lại, chẳng hạn như khi bạn nhập vào đầu vào.
import { useReducer } from 'react'; function createInitialState(username) { const initialTodos = []; for (let i = 0; i < 50; i++) { initialTodos.push({ id: i, text: username + "'s task #" + (i + 1) }); } return { draft: '', todos: initialTodos, }; } function reducer(state, action) { switch (action.type) { case 'changed_draft': { return { draft: action.nextDraft, todos: state.todos, }; }; case 'added_todo': { return { draft: '', todos: [{ id: state.todos.length, text: state.draft }, ...state.todos] } } } throw Error('Unknown action: ' + action.type); } export default function TodoList({ username }) { const [state, dispatch] = useReducer( reducer, username, createInitialState ); return ( <> <input value={state.draft} onChange={e => { dispatch({ type: 'changed_draft', nextDraft: e.target.value }) }} /> <button onClick={() => { dispatch({ type: 'added_todo' }); }}>Add</button> <ul> {state.todos.map(item => ( <li key={item.id}> {item.text} </li> ))} </ul> </> ); }
Khắc phục sự cố
Tôi đã gửi một action, nhưng nhật ký cho tôi giá trị trạng thái cũ
Gọi hàm dispatch
không thay đổi trạng thái trong mã đang chạy:
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Yêu cầu kết xuất lại với 43
console.log(state.age); // Vẫn là 42!
setTimeout(() => {
console.log(state.age); // Cũng là 42!
}, 5000);
}
Điều này là do trạng thái hoạt động như một ảnh chụp nhanh. Cập nhật trạng thái yêu cầu một kết xuất khác với giá trị trạng thái mới, nhưng không ảnh hưởng đến biến JavaScript state
trong trình xử lý sự kiện đang chạy của bạn.
Nếu bạn cần đoán giá trị trạng thái tiếp theo, bạn có thể tính toán nó theo cách thủ công bằng cách tự gọi reducer:
const action = { type: 'incremented_age' };
dispatch(action);
const nextState = reducer(state, action);
console.log(state); // { age: 42 }
console.log(nextState); // { age: 43 }
Tôi đã gửi một action, nhưng màn hình không cập nhật
React sẽ bỏ qua bản cập nhật của bạn nếu trạng thái tiếp theo bằng với trạng thái trước đó, như được xác định bởi so sánh Object.is
. Điều này thường xảy ra khi bạn thay đổi trực tiếp một đối tượng hoặc một mảng trong trạng thái:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Sai: đột biến đối tượng hiện có
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Sai: đột biến đối tượng hiện có
state.name = action.nextName;
return state;
}
// ...
}
}
Bạn đã đột biến một đối tượng state
hiện có và trả về nó, vì vậy React đã bỏ qua bản cập nhật. Để khắc phục điều này, bạn cần đảm bảo rằng bạn luôn cập nhật các đối tượng trong trạng thái và cập nhật các mảng trong trạng thái thay vì đột biến chúng:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Đúng: tạo một đối tượng mới
return {
...state,
age: state.age + 1
};
}
case 'changed_name': {
// ✅ Đúng: tạo một đối tượng mới
return {
...state,
name: action.nextName
};
}
// ...
}
}
Một phần trạng thái reducer của tôi trở thành không xác định sau khi gửi
Đảm bảo rằng mọi nhánh case
sao chép tất cả các trường hiện có khi trả về trạng thái mới:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
...state, // Đừng quên điều này!
age: state.age + 1
};
}
// ...
Nếu không có ...state
ở trên, trạng thái tiếp theo được trả về sẽ chỉ chứa trường age
và không có gì khác.
Toàn bộ trạng thái reducer của tôi trở thành không xác định sau khi gửi
Nếu trạng thái của bạn bất ngờ trở thành undefined
, có thể bạn đang quên return
trạng thái trong một trong các trường hợp hoặc loại action của bạn không khớp với bất kỳ câu lệnh case
nào. Để tìm lý do, hãy đưa ra một lỗi bên ngoài switch
:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ...
}
case 'edited_name': {
// ...
}
}
throw Error('Unknown action: ' + action.type);
}
Bạn cũng có thể sử dụng trình kiểm tra kiểu tĩnh như TypeScript để bắt các lỗi như vậy.
Tôi gặp lỗi: “Quá nhiều lần kết xuất lại”
Bạn có thể gặp lỗi cho biết: Too many re-renders. React limits the number of renders to prevent an infinite loop.
(Quá nhiều lần kết xuất lại. React giới hạn số lần kết xuất để ngăn chặn vòng lặp vô hạn.) Thông thường, điều này có nghĩa là bạn đang gửi một action vô điều kiện trong quá trình kết xuất, vì vậy thành phần của bạn đi vào một vòng lặp: kết xuất, gửi (gây ra kết xuất), kết xuất, gửi (gây ra kết xuất), v.v. Rất thường xuyên, điều này là do một sai lầm trong việc chỉ định một trình xử lý sự kiện:
// 🚩 Sai: gọi trình xử lý trong quá trình kết xuất
return <button onClick={handleClick()}>Click me</button>
// ✅ Đúng: chuyển trình xử lý sự kiện xuống
return <button onClick={handleClick}>Click me</button>
// ✅ Đúng: chuyển một hàm nội tuyến xuống
return <button onClick={(e) => handleClick(e)}>Click me</button>
Nếu bạn không thể tìm thấy nguyên nhân của lỗi này, hãy nhấp vào mũi tên bên cạnh lỗi trong bảng điều khiển và xem qua ngăn xếp JavaScript để tìm lệnh gọi hàm dispatch
cụ thể chịu trách nhiệm cho lỗi.
Hàm reducer hoặc hàm khởi tạo của tôi chạy hai lần
Trong Chế độ nghiêm ngặt, React sẽ gọi các hàm reducer và hàm khởi tạo của bạn hai lần. Điều này sẽ không phá vỡ mã của bạn.
Hành vi chỉ dành cho phát triển này giúp bạn giữ cho các thành phần thuần túy. React sử dụng kết quả của một trong các lệnh gọi và bỏ qua kết quả của lệnh gọi kia. Miễn là thành phần, trình khởi tạo và các hàm reducer của bạn là thuần túy, điều này sẽ không ảnh hưởng đến logic của bạn. Tuy nhiên, nếu chúng vô tình không thuần túy, điều này sẽ giúp bạn nhận thấy những sai lầm.
Ví dụ: hàm reducer không thuần túy này đột biến một mảng trong trạng thái:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// 🚩 Sai lầm: đột biến trạng thái
state.todos.push({ id: nextId++, text: action.text });
return state;
}
// ...
}
}
Vì React gọi hàm reducer của bạn hai lần, bạn sẽ thấy todo đã được thêm hai lần, vì vậy bạn sẽ biết rằng có một sai lầm. Trong ví dụ này, bạn có thể sửa sai lầm bằng cách thay thế mảng thay vì đột biến nó:
function reducer(state, action) {
switch (action.type) {
case 'added_todo': {
// ✅ Đúng: thay thế bằng trạng thái mới
return {
...state,
todos: [
...state.todos,
{ id: nextId++, text: action.text }
]
};
}
// ...
}
}
Bây giờ hàm reducer này là thuần túy, việc gọi nó thêm một lần không tạo ra sự khác biệt trong hành vi. Đây là lý do tại sao React gọi nó hai lần giúp bạn tìm thấy những sai lầm. Chỉ các hàm thành phần, trình khởi tạo và reducer cần phải thuần túy. Các trình xử lý sự kiện không cần phải thuần túy, vì vậy React sẽ không bao giờ gọi các trình xử lý sự kiện của bạn hai lần.
Đọc giữ cho các thành phần thuần túy để tìm hiểu thêm.