Xếp hàng đợi cho một chuỗi các cập nhật state
Thiết lập một biến state sẽ đưa một lần render khác vào hàng đợi. Nhưng đôi khi bạn có thể muốn thực hiện nhiều thao tác trên giá trị đó trước khi đưa lần render tiếp theo vào hàng đợi. Để làm điều này, sẽ hữu ích khi hiểu cách React gom nhóm các cập nhật state.
Bạn sẽ được học
- ”Batching” là gì và React sử dụng nó như thế nào để xử lý nhiều cập nhật state
- Cách áp dụng liên tiếp nhiều cập nhật cho cùng một biến state
React gom nhóm các cập nhật state
Bạn có thể nghĩ rằng việc nhấp vào nút “+3” sẽ tăng bộ đếm ba lần bởi vì nó gọi setNumber(number + 1)
ba lần:
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 1); setNumber(number + 1); setNumber(number + 1); }}>+3</button> </> ) }
Tuy nhiên, như bạn có thể nhớ lại từ phần trước, giá trị state của mỗi lần render được cố định, vì vậy giá trị của number
bên trong event handler của lần render đầu tiên luôn là 0
, bất kể bạn gọi setNumber(1)
bao nhiêu lần:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
Nhưng có một yếu tố khác đang tác động ở đây. React chờ đợi cho đến khi tất cả code trong các event handler đã chạy xong trước khi xử lý các cập nhật state của bạn. Đây là lý do tại sao việc render lại chỉ xảy ra sau tất cả các lệnh gọi setNumber()
này.
Điều này có thể gợi nhớ đến một người phục vụ nhận đơn hàng tại nhà hàng. Người phục vụ không chạy đến bếp ngay khi nghe thấy món đầu tiên của bạn! Thay vào đó, họ để bạn hoàn thành đơn hàng, để bạn thực hiện các thay đổi, và thậm chí nhận đơn hàng từ những người khác tại bàn.

Illustrated by Rachel Lee Nabors
Điều này cho phép bạn cập nhật nhiều biến state—thậm chí từ nhiều component—mà không kích hoạt quá nhiều lần render lại. Nhưng điều này cũng có nghĩa là UI sẽ không được cập nhật cho đến sau khi event handler của bạn, và bất kỳ code nào trong đó, hoàn thành. Hành vi này, còn được gọi là batching, làm cho ứng dụng React của bạn chạy nhanh hơn nhiều. Nó cũng tránh việc phải đối phó với các lần render “chưa hoàn thành” gây nhầm lẫn khi chỉ một số biến được cập nhật.
React không gom nhóm qua nhiều sự kiện có chủ ý như click—mỗi click được xử lý riêng biệt. Hãy yên tâm rằng React chỉ thực hiện batching khi nó thường an toàn để làm như vậy. Điều này đảm bảo rằng, ví dụ, nếu click nút đầu tiên vô hiệu hóa một form, click thứ hai sẽ không gửi nó lần nữa.
Cập nhật cùng một state nhiều lần trước lần render tiếp theo
Đây là một trường hợp sử dụng không phổ biến, nhưng nếu bạn muốn cập nhật cùng một biến state nhiều lần trước lần render tiếp theo, thay vì truyền giá trị state tiếp theo như setNumber(number + 1)
, bạn có thể truyền một function tính toán state tiếp theo dựa trên state trước đó trong hàng đợi, như setNumber(n => n + 1)
. Đây là cách để nói với React “làm điều gì đó với giá trị state” thay vì chỉ thay thế nó.
Hãy thử tăng bộ đếm ngay bây giờ:
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); }}>+3</button> </> ) }
Ở đây, n => n + 1
được gọi là updater function. Khi bạn truyền nó cho một state setter:
- React đưa function này vào hàng đợi để được xử lý sau khi tất cả code khác trong event handler đã chạy.
- Trong lần render tiếp theo, React duyệt qua hàng đợi và cung cấp cho bạn state cuối cùng đã cập nhật.
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
Đây là cách React xử lý các dòng code này khi thực thi event handler:
setNumber(n => n + 1)
:n => n + 1
là một function. React thêm nó vào hàng đợi.setNumber(n => n + 1)
:n => n + 1
là một function. React thêm nó vào hàng đợi.setNumber(n => n + 1)
:n => n + 1
là một function. React thêm nó vào hàng đợi.
Khi bạn gọi useState
trong lần render tiếp theo, React duyệt qua hàng đợi. State number
trước đó là 0
, vì vậy đó là những gì React truyền cho updater function đầu tiên làm tham số n
. Sau đó React lấy giá trị trả về của updater function trước đó và truyền nó cho updater tiếp theo làm n
, và cứ thế:
cập nhật trong hàng đợi | n | trả về |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React lưu trữ 3
là kết quả cuối cùng và trả về nó từ useState
.
Đây là lý do tại sao việc nhấp “+3” trong ví dụ trên đúng cách tăng giá trị lên 3.
Điều gì xảy ra nếu bạn cập nhật state sau khi thay thế nó
Còn event handler này thì sao? Bạn nghĩ number
sẽ là gì trong lần render tiếp theo?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); }}>Increase the number</button> </> ) }
Đây là những gì event handler này yêu cầu React thực hiện:
setNumber(number + 5)
:number
là0
, vì vậysetNumber(0 + 5)
. React thêm “thay thế bằng5
” vào hàng đợi của nó.setNumber(n => n + 1)
:n => n + 1
là một updater function. React thêm function đó vào hàng đợi của nó.
Trong lần render tiếp theo, React duyệt qua hàng đợi state:
cập nhật trong hàng đợi | n | trả về |
---|---|---|
”thay thế bằng 5 ” | 0 (không sử dụng) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React lưu trữ 6
là kết quả cuối cùng và trả về nó từ useState
.
Điều gì xảy ra nếu bạn thay thế state sau khi cập nhật nó
Hãy thử thêm một ví dụ nữa. Bạn nghĩ number
sẽ là gì trong lần render tiếp theo?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
import { useState } from 'react'; export default function Counter() { const [number, setNumber] = useState(0); return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); setNumber(n => n + 1); setNumber(42); }}>Increase the number</button> </> ) }
Đây là cách React xử lý các dòng code này khi thực thi event handler này:
setNumber(number + 5)
:number
là0
, vì vậysetNumber(0 + 5)
. React thêm “thay thế bằng5
” vào hàng đợi của nó.setNumber(n => n + 1)
:n => n + 1
là một updater function. React thêm function đó vào hàng đợi của nó.setNumber(42)
: React thêm “thay thế bằng42
” vào hàng đợi của nó.
Trong lần render tiếp theo, React duyệt qua hàng đợi state:
cập nhật trong hàng đợi | n | trả về |
---|---|---|
”thay thế bằng 5 ” | 0 (không sử dụng) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
”thay thế bằng 42 ” | 6 (không sử dụng) | 42 |
Sau đó React lưu trữ 42
là kết quả cuối cùng và trả về nó từ useState
.
Để tóm tắt, đây là cách bạn có thể nghĩ về những gì bạn đang truyền cho state setter setNumber
:
- Một updater function (ví dụ
n => n + 1
) được thêm vào hàng đợi. - Bất kỳ giá trị nào khác (ví dụ số
5
) thêm “thay thế bằng5
” vào hàng đợi, bỏ qua những gì đã có trong hàng đợi.
Sau khi event handler hoàn thành, React sẽ kích hoạt một lần render lại. Trong quá trình render lại, React sẽ xử lý hàng đợi. Các updater function chạy trong quá trình rendering, vì vậy các updater function phải thuần khiết và chỉ trả về kết quả. Đừng cố gắng set state từ bên trong chúng hoặc chạy các side effect khác. Trong Strict Mode, React sẽ chạy mỗi updater function hai lần (nhưng bỏ qua kết quả lần thứ hai) để giúp bạn tìm ra lỗi.
Quy ước đặt tên
Thông thường người ta đặt tên cho tham số của updater function bằng các chữ cái đầu của biến state tương ứng:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
Nếu bạn thích code chi tiết hơn, một quy ước phổ biến khác là lặp lại tên đầy đủ của biến state, như setEnabled(enabled => !enabled)
, hoặc sử dụng tiền tố như setEnabled(prevEnabled => !prevEnabled)
.
Tóm tắt
- Thiết lập state không thay đổi biến trong lần render hiện tại, nhưng nó yêu cầu một lần render mới.
- React xử lý các cập nhật state sau khi các event handler đã chạy xong. Điều này được gọi là batching.
- Để cập nhật một số state nhiều lần trong một sự kiện, bạn có thể sử dụng updater function
setNumber(n => n + 1)
.
Challenge 1 of 2: Sửa bộ đếm yêu cầu
Bạn đang làm việc trên một ứng dụng thị trường nghệ thuật cho phép người dùng gửi nhiều đơn hàng cho một tác phẩm nghệ thuật cùng một lúc. Mỗi khi người dùng nhấn nút “Buy”, bộ đếm “Pending” sẽ tăng lên một. Sau ba giây, bộ đếm “Pending” sẽ giảm xuống, và bộ đếm “Completed” sẽ tăng lên.
Tuy nhiên, bộ đếm “Pending” không hoạt động như dự định. Khi bạn nhấn “Buy”, nó giảm xuống -1
(điều này không thể xảy ra!). Và nếu bạn nhấp nhanh hai lần, cả hai bộ đếm dường như hoạt động không thể đoán trước.
Tại sao điều này xảy ra? Hãy sửa cả hai bộ đếm.
import { useState } from 'react'; export default function RequestTracker() { const [pending, setPending] = useState(0); const [completed, setCompleted] = useState(0); async function handleClick() { setPending(pending + 1); await delay(3000); setPending(pending - 1); setCompleted(completed + 1); } return ( <> <h3> Pending: {pending} </h3> <h3> Completed: {completed} </h3> <button onClick={handleClick}> Buy </button> </> ); } function delay(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); }