Hiệu ứng hay hệ quả có thể đến sau hoặc không, nhưng nguyên nhân thì chắc chắn đến trước.
Effect có vai trò như một cửa thoát hiểm trong mô hình của React. Chúng cho phép bạn “bước ra ngoài” React và đồng bộ các component với các hệ thống bên ngoài như giao tiếp các thư viện bên thứ ba, với mạng hay với DOM. Nếu component không kết nối với hệ thống ngoại lai nào khác, bạn không nên sử dụng Effect. Loại bỏ các Effect không cần thiết giúp code dễ theo dõi logic, chạy nhanh và ít lỗi hơn.
Làm thế nào để loại bỏ Effect không cần thiết
Hai trường hợp thường gặp mà bạn không cần dùng đến Effect:
- Không cần Effect để chuyển đổi dữ liệu cho render. Chẳng hạn, bạn muốn lọc một danh sách trước mỗi lần hiển thị. Bạn có thể thấy hấp dẫn với ý tưởng viết một Effect để cập nhật biến state mỗi khi danh sách này thay đổi. Tuy nhiên, làm vậy thực ra không hiệu quả. Mỗi khi state của component thay đổi, React trước tiên sẽ gọi các hàm để tính toán những gì sẽ được hiển thị, sau đó mới “commit” những thay đổi này lên DOM. Tiếp đến React chạy các Effect. Nếu Effect lại tiếp tục cập nhật state, toàn bộ quá trình lại chạy lại từ đầu. Để loại bỏ mọi render thừa, hãy chuyển đổi toàn bộ dữ liệu ở ngay đầu component. Những hàm chuyển đổi đó sẽ tự động chạy lại mỗi khi props hay state thay đổi.
- Không cần Effect để xử lý event từ người dùng. Giả dụ mỗi khi người dùng mua một sản phẩm, bạn muốn gửi một request POST đến
/api/buy
và hiển thị một notification. Mọi xử lý nên được đặt trong event handler tương ứng của nút “Mua”, chứ không phải Effect.
Chúng ta hãy tìm hiểu các ví dụ cụ thể sau để hiểu rõ hơn.
Cập nhật state dựa trên props hoặc state
Giả dụ bạn có một component với 2 biến state: firstName
và lastName
, và muốn hiển thị tên đầy đủ fullName
bằng cách nối hai biến trên. fullName
phải tự động cập nhật mỗi firstName
hoặc lastName
thay đổi. Trực giác mách bảo bạn nên cập nhật fullName
trong một Effect.
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 🔴 Cần tránh: state và Effect không cần thiết
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
Mọi thứ có vẻ phức tạp hơn bình thường, và kém hiệu quả nữa: toàn bộ component phải render khi khai báo state cho fullName
, rồi render tiếp khi nó được cập nhật bởi Effect. Hãy xoá cả state và Effect:
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Tốt: tính ngay khi render
const fullName = firstName + ' ' + lastName;
// ...
}
Khi thứ gì đó có thể được tính toán từ props hoặc state đã tồn tại, đừng tạo state. Thay vào đó hay tính toán khi render. Điều này giúp code nhanh hơn (loại các tính toán “chồng nhau”), đơn giản hơn (ít code hơn), và ít lỗi hơn (tránh được các bug khi các state không được đồng bộ với nhau). Nếu đã đọc được tới đây mà mọi thứ vẫn mù mịt, bạn có thể phải dừng lại và học cách “suy nghĩ theo React” trước.
Cache các tính toán phức tạp
Phần trên khuyên ta nên tính toán các giá trị ngay khi render. Vậy nếu các tính toán đó rất tốn tài nguyên thì sao? Component TodoList
sau tính toán visibleTodos
bằng cách đem props todos
đi lọc theo props filter
. Bạn dễ bị cuốn theo suy nghĩ lưu kết quả tính toán vào state và update trong Effect:
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 Cần tránh: state và Effect không cần thiết
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
Giống như ví dụ trước, cách làm này vừa thừa vừa phí. Đầu tiên, cần loại bỏ state và Effect:
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Ổn nếu getFilteredTodos() nhanh.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
Code trên sẽ gặp vấn đề nếu getFilteredTodos
chạy chậm hoặc có rất nhiều todos
. Trong trường hợp đó, bạn sẽ không muốn tính lại getFilteredTodos()
nêú state không liên quan kiểu như newTodo
thay đổi, mà thay vào đó cache (hay “memoize” kết quả tính toán lại, bằng useMemo
:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ Không chạy lại trừ khi todos hay filter thay đổi
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
Hay ngắn gọn chỉ với 1 dòng:
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ Không chạy lại trừ khi todos hay filter thay đổi
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
Code trên bảo React không cần chạy alij hàm bên trong useMemo
trừ trường hợp todos
hoặc filter
thay đổi. React sẽ ghi nhớ kết quả tính toán từ getFilteredTodos()
trong lần render khởi tạo. Những lần render sau, nó sẽ kiểm tra xem todos
hoặc filter
có khác không. Nếu chung vẫn giống lần render trước, useMemo
sẽ trả lại kết quả trước đã được lưu sẵn. Nếu chúng khác, React gọi chạy lại hàm bên trong, trả về đồng thời lưu lại kết quả tính toán mới. Lưu ý hàm bên trong useMemo
phải là hàm thuần khiết - pure function.
Reset toàn bộ state khi prop thay đổi
Component ProfilePage
nhận được prop userId
. Nó bao gồm một input để nhập bình luận, lưu trong state comment
. Một buổi sáng bạn nhận ra khi trang này thay đổi hồ sơ từ người này sang người khác, comment
không thay đổi. Sẽ không dễ chịu gì nếu bình luận của người này lại được đăng trong hồ sơ của một người khác. Để sửa, bạn sẽ xoá comment
mỗi khi userId
thay đổi:
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 Cần tránh: Reset state theo prop trong Effect
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
Cách làm này không hiệu quả bởi ProfilePage
sẽ render giá trị cũ trước rồi mới render lại. Nó còn phức tạp nữa bởi bạn sẽ phải xoá state trong mọi component trong ProfilePage
. Ví dụ, nên UI phần bình luận có nhiều cấp, bạn có thể phải xoá cả state bình luận lồng nhau.
Thay vào đó, bạn có thể nói React rằng mỗi hồ sơ người dùng về mặt khái niệm là khác nhau hoàn toàn bằng cách sử dụng key
. Chia component làm hai và truyền key
từ component ngoài vào component trong:
export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ State này và mọi state ở dưới sẽ được reset tự động
const [comment, setComment] = useState('');
// ...
}
Thông thường, React bảo toàn state khi component giống nhau được render tại vị trí giống nhau. Bằng cách truyền userId
như key
vào component Profile
, bạn yêu cầu React đối xử Profile
với userId
khác nhau như những component khác nhau không dùng chung bất cứ state nào. Mỗi khi key thay đổi, React tạo lại DOM và reset toàn bộ state của Profile
và các con của nó. Kết quả comment
được xoá tư động mỗi khi chuyển sang hồ sơ khác.
Đáng chú ý là chỉ ProfilePage
cha được export trong project. Component nào render ProfilePage
không cần phải truyền key cho nó, mà chỉ truyền userId
như prop bình thường. Việc ProfilePage
truyền key
cho Profile
bên trong chỉ là cách thực thi trong nội bộ của nó thôi.
Điều chỉnh một số state theo prop
Đôi khi, bạn muốn reset hoặc điều chỉnh chỉ một số state, không phải toàn bộ, mỗi khi prop thay đổi.
Component List
sau đây nhận vào một danh sách items
, mà lưu danh sách các item được chọn bởi người dùng vào state selection
. Bạn muốn reset selection
về null
mỗi khi items
nhận vào một mảng mới:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Cần tránh: Điều chỉnh state theo prop trong Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
Mỗi khi items
thay đổi, List
và các component con sẽ render lại với giá trị cũ của selection
trước. Sau đó React mới cập nhật DOM và chạy Effect. Cuối cùng, setSelection(null)
được gọi khiến render lại List
và các component con thêm một lần nữa.
Cách làm đúng là xoá Effect rồi điều chỉnh state ngay trong render.
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Tốt hơn: Điều chỉnh state ngay khi render
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
Lưu trữ thông tin từ lần render trước có vẻ là một kỹ thuật khó hiểu, nhưng nó ổn hơn so với việc cập nhật lại state cũ trong Effect. Trong ví dụ trên, setSelection
được gọi trực tiếp khi render. React sẽ render lại List
ngay sau khi return
. React không render lại các con của List
và cập nhật DOM ngay, bỏ qua lần render với giá trị cũ của selection
.
Khi bạn cập nhật một component trong render, React vứt bỏ hết JSX trả về và cố gắng render lại ngay. Để phòng tránh các lần thử lại bị xếp chồng, React chỉ cho phép bạn cập nhật state của component giống nhau trong mỗi lần render. Nếu bạn cập nhật state của component khác, bạn sẽ gặp lỗi. Điều kiện như items !== prevItems
là cần thiết để tránh vòng lặp. Điều chỉnh state có thể làm như này còn các hiệu ứng phụ khác như thay đổi DOM hay đặt hẹn giờ nên được giữ trong event handler hoặc Effect để giữ cho component hoạt động ổn định.
Mặc dù pattern này hiệu quả hơn Effect, chúng không nên được sử dụng nhiều. Điều chỉnh state dựa trên props hoặc state khác khiến luồng dữ liệu khó theo dõi và debug. Luôn kiểm tra rằng bạn có thể reset toàn bộ state bằng key
hoặc tính toán mọi thứ khi render không. Chẳng hạn, thay vì lưu (và reset) item được chọn, bạn có thể lưu ID của nó:
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Tốt nhất: Tính toán mọi thứ ngay trong render
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
Giờ bạn không cần phải “điều chỉnh” state nữa. Nếu ID nằm trong danh sách, item tương ứng vẫn được chọn. Nếu không, selection
sẽ bằng null
. Hành vi này khác biệt chút ít, nhưng rõ ràng là ổn hơn vì mọi thay đổi của items
lúc này đều phản ánh ngay vào selection
. Lưu ý nhỏ là bạn cần sử dụng selection
trong mọi logic sau đó bởi selectedId
có thể không tồn tại.
Chia sẻ logic giữa nhiều event handler
Bạn có một trang sản phẩm với 2 nút Mua
và Giỏ hàng
, cả hai đều dùng để mua hàng. Bạn muốn hiện một thông báo mỗi khi người dùng thêm sản phẩm vào giỏ hàng. Gọi hàm showNotification()
trong handler của cả 2 nút có vẻ lặp lại, nên bạn thấy sẽ hơn hay nếu đặt logic này trong Effect:
function ProductPage({ product, addToCart }) {
// 🔴 Cần tránh: Đặt logic gắn liền với event trong Effect
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
Effect này không chỉ thừa, mà còn dễ tạo ra bug. Giả dụ app của bạn có khả năng “ghi nhớ” giỏ hàng kể cả tải lại các trang. Nếu bạn thêm hàng vào giỏ và tải lại trang, thông báo sẽ lại xuất hiện. Điều này bởi product.isInCart
luôn true
khi trang tải, nên Effect gọi showNotification()
.
Khi bạn không chắc chắn rằng code nào nên nằm trong Effect hoặc trong hàm xử lý event, hãy tự hỏi tại sao cần phải chạy đoạn code này. Chỉ sử dụng Effect cho đoạn code cần chạy bởi vì component phải hiển thị cho người dùng. Trong ví dụ này, thông báo nên xuất hiện bởi vị người dùng nhấn nút, không phải bởi trang được hiển thị! Xoá Effect và đưa logic chung vào một hàm mà bạn sẽ gọi từ cả hai hàm xử lý event:
function ProductPage({ product, addToCart }) {
// ✅ Tốt: Logic gắn liền với event nằm trong handler
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}
Effect được xoá và bug được fix.
Gửi một request POST
Form
sau gửi hai loại request POST. Nó gửi event đo lường khi mount. Khi bạn điền xong form và nhấn vào nút Submit, nó sẽ gửi một request POST tới /api/register
:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Tốt: Logic này chỉ chạy khi component phải xuất hiện
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
// 🔴 Cần tránh: Logic gắn với event lại đặt trong Effect
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);
function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}
Hãy đánh giá đoạn code bởi cùng tiêu chuẩn cho ví dụ trước.
Request POST đo lường nên nằm trong Effect, bởi lý do để gửi sự kiện này là sự xuất hiện của Form
.
Tuy nhiên, request POST lên /api/register
thì không phải vì sự xuất hiện của form, mà vì người dùng bấm nút gửi. Do đó xoá Effect thứ 2 đi và chuyển request này vào trong event handler:
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ Tốt: Logic này chỉ chạy khi component phải xuất hiện
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);
function handleSubmit(e) {
e.preventDefault();
// ✅ Tốt: Logic gắn với event nằm trong handler
post('/api/register', { firstName, lastName });
}
// ...
}
Vậy mỗi khi bạn cần quyết định xem một đoạn logic nằm trong event handler hay Effect, câu hỏi chính cần trả lời là đây là loại logic gì từ góc nhìn của người dùng. Nếu đoạn logic được gọi bởi một tương tác xác định, đặt chúng trong event handler. Nếu chúng được gây ra bởi sự xuất hiện trên màn hình của user, đặt chúng trong Effect.
“Domino” Effect
Xem xét đoạn code sau:
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 Cần tránh: một state thay đổi kích hoạt một chuỗi Effect
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
// ...
Có 2 vấn đề:
Một là, code này rất kém hiệu quả: component phải render lại mỗi khi gọi set
gì đó thành chuỗi. Trong trường hợp xấu nhất, setCard
→ render → setGoldCardCount
→ render → setRound
→ render → setIsGameOver
→ render, tạo thêm 3 lần render thừa.
Kể cả khi nó không chậm, khi code phát triển lên, bạn sẽ gặp những trường hơp “chuỗi” này sẽ không còn đáp ứng yêu cầu nữa. Tưởng tượng nếu bạn thêm tính năng quay lại lịch sử từng nước đi bằng cách cập nhật card
về một nước đi trong quá khứ, dẫn đến kích hoạt chuỗi Effect chạy lại và thay đổi dữ liệu đang hiển thị. Cách viết như vậy rất khó maintain và debug.
Tốt hơn là bạn nên cố gắng tính nhiều nhất có thể khi render, và điều chỉnh state trong event handler:
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ Tính toán mọi thứ ngay khi render
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ Tính toán state tiếp theo ngay trong event handler
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ...
Cần nhớ rằng trong event handler, state hoạt động giống như một trạng thái xác định, nghĩa là kể cả khi bạn gọi setRound(round + 1)
, thì biến round
vẫn chứa giá trị cũ khi người dùng bấm nút mà thôi. Nếu bạn muốn sử dụng giá trị tiếp theo cho tính toán, hãy tự định nghĩa một biến kiểu như const nextRound = round + 1
.
Tuy nhiên không phải lúc nào bạn cũng biết chính xác trạng thái tiếp theo là gì trong event handler. Chẳng hạn, hãy tưởng tượng một form có nhiều dropdown mà trong đó các tuỳ chọn của dropdown sau phụ thuộc vào lựa chọn từ dropdown trước (ví dụ khi điền tỉnh - huyện - xã). Lúc ấy mô hình xếp chuỗi Effect lại phù hợp để đồng bộ dữ liệu.
Khởi tạo app
Một số logic chỉ nên chạy một lần duy nhất khi app khởi tạo. Bạn nên đặt chúng trong Effect ở component cao nhất.
function App() {
// 🔴 Cần tránh: Effect chứa logic chỉ chạy một lần duy nhất
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}
Tuy nhiên, bạn nhanh chóng nhận ra nó vẫn được chạy hai lần ở chế độ development. Điều này gây ra rắc rối - ví dụ như nó huỷ xác thực token khi chạy hai lần. Nhìn chung, component nên bền bỉ với việc bị mount lại. Nếu một số logic chỉ được phép chạy một lần khi app khởi tạo chứ không phải một lần khi app mount, bạn nên dùng một biến để giám sát việc khởi tạo:
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Chỉ chạy một lần duy nhất khi khởi động app
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
Bạn thậm chí còn có thể chạy chúng trong khi khởi tạo module và trước khi app render:
if (typeof window !== 'undefined') { // Kiểm tra có phải môi trường browser không
// ✅ Chỉ chạy một lần duy nhất khi khởi động app
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
Báo hiệu cho component cha khi state thay đổi
Bạn có một component Toggle
với state isOn
có thể true
hoặc false
. Bạn muốn báo hiệu cho component cha mỗi khi Toggle
đổi trạng thái, vì thế bạn bộc lộ event onChange
và gọi nó từ Effect:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
// 🔴 Cần tránh: onChange chạy muộn quá
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])
function handleClick() {
setIsOn(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}
// ...
}
Toggle
cập nhật state trước, React cập nhật màn hình. Tiếp đến React chạy Effect, gọi onChange
truyền vào từ component cha. Lúc này component cha lại cập nhật state của nó, dẫn đến một lần render nữa. Sẽ tốt hơn nếu mọi thứ diễn ra chỉ trong một lần render.
Xoá Effect và cập nhật trạng thái của cả hai component trong cùng một event handler:
function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);
function updateToggle(nextIsOn) {
// ✅ Tốt: chạy mọi cập nhật ngay trong event khiến state thay đổi
setIsOn(nextIsOn);
onChange(nextIsOn);
}
function handleClick() {
updateToggle(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}
// ...
}
Với cách làm này, cả component Toggle
và cha đều update trạng thái trong event. React cập nhật theo lô từ nhiều component khác nhau, nên chỉ có một lần render diễn ra.
Bạn cũng có thể xoá luôn state và nhận isOn
từ component cha:
// ✅ Cũng tốt: state cho component cha toàn quyền xử lý
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}
// ...
}
“Đưa state lên trên” cho phép cha toàn quyền điều khiển Toggle
từ state của cha. Điều này cũng có nghĩa là component cha sẽ phải thêm nhiều logic hơn, nhưng tổng thể sẽ có ít state phải lo hơn. Mỗi khi bạn đang tìm cách đồng bộ state từ hai chỗ khác nhau, đó là dấu hiệu cho thấy cần đưa state lên trên!
Đưa dữ liệu cho component cha
Component Child
gọi về dữ liệu và muốn truyền dữ liệu đó cho component cha:
function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}
function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 Cần tránh: truyền dữ liệu từ con lên cha trong Effect
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}
Trong React, dữ liệu chảy từ component cha đến các con. Khi gặp lỗi, bạn có thể mò được thông tin lỗi đến từ đâu bằng cách đi ngược dần lên theo chuỗi component cho tới khi bạn tìm được “thủ phạm” truyền sai prop hoặc lỗi state. Nếu bạn dùng Effect để cập nhật dữ liệu cho cha từ con, luồng dữ liệu trở nên rất khó theo dõi. Nếu cả con và cha đều cần cùng một dữ liệu, tốt nhất hãy để cha lấy dữ liệu và truyền cho con:
function Parent() {
const data = useSomeAPI();
// ...
// ✅ Tốt: dữ liệu truyền từ cha xuống con
return <Child data={data} />;
}
function Child({ data }) {
// ...
}
Đăng ký theo dõi nguồn bên ngoài
Nhiều khi component có thể cần đăng ký một nguồn dữ liệu nào đó nằm ngoài state React, như một thư viện bên thứ ba hoặc API của browser. Nguồn dữ liệu này có thể thay đổi tuỳ ý mà React không biết, đòi hỏi bạn sử dụng Effect để theo dõi nguồn dữ liệu:
function useOnlineStatus() {
// Không tốt: viết hàm theo dõi thủ công
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}
updateState();
window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
Mặc dù cách làm này tương đối phổ biến, React trang bị sẵn cho chúng ta một Hook chuyên trách cho công việc đăng ký nguồn dữ liệu ngoài. Xoá Effect đi và thay thế bằng useSyncExternalStore
:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
// ✅ Tốt: đăng ký theo dõi nguồn bên ngoài với Hook hỗ trợ sẵn bởi React
return useSyncExternalStore(
subscribe, // React không đăng ký lại trừ phi bạn truyền hàm khác
() => navigator.onLine, // Hàm này chạy ở client
() => true // Hàm này chạy với server
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
Tải về dữ liệu
Rất nhiều app sử dụng Effect để khởi động tải về dữ liệu, điển hình như sau:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
// 🔴 Cần tránh: Kéo dữ liệu mà quên dọn dẹp
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
Đoạn code trên có bug. Tưởng tượng bạn nhập nhanh tìm kiếm "hello"
. query
sẽ thay đổi từ "h"
, thành "he"
, "hel"
, "hell"
, và "hello"
. Mỗi thay đổi sẽ khởi động việc gọi dữ liệu cho riêng mình, nhưng không có gì đảm bảo các response sẽ về theo đúng thứ tự đó. Chẳng hạn, response cho "hell"
có thể về sau response của "hello"
, dẫn đến hiển thị sai kết quả tìm kiếm. Đây là một dạng "race condition"
: hai request khác nhau chạy đua và kết quả trả về theo thứ tự khác với những gì bạn mong đợi.
Để khắc phục, bạn cần thêm một hàm dọn dẹp để phớt lờ những response cũ:
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
Xử lý race condition không chỉ là thách thức duy nhất khi thực hành tải về dữ liệu. Bạn phải tính đến cache dữ liệu trả về ra sao (để người dùng có thể nhấn Back và thấy ngay màn hình trước đó thay vì biểu tượng xoay xoay), làm sao để gọi dữ liệu phía server (để render được HTML thay vì biểu tượng xoay xoay), và làm sao để phòng ngừa thác đổ dữ liệu (network waterfall - component con phải chờ tất cả cha của nó kéo dữ liệu xong mới đến lượt nó). Những vấn đề trên xuất hiện với mọi thư viện UI chứ không riêng gì React. Giải quyết các vấn đề này không dễ dàng gì, vì thế các framework hiện đại thường cung cấp sẵn cơ chế gọi về dữ liệu thay vì viết Effect ngay trong component.
Nếu bạn không sử dung framework (và không muốn tự build một cái) mà vẫn muốn gọi về dữ liệu một cách thân thiện và hiệu quả, tham khảo custom Hook sau đây:
function SearchResults({ query }) {
const [page, setPage] = useState(1);
const params = new URLSearchParams({ query, page });
const results = useData(`/api/search?${params}`);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}
Bạn sẽ phải thêm logic để xử lý lỗi và theo dõi nội dung đã tải xong hay chưa. Dù giải pháp này chưa hoàn hảo, nhưng đưa logic kéo về dữ liệu ra riêng một custom Hook giúp ta tuỳ biến chiến thuật kéo về dữ liệu một cách dễ dàng và hiệu quả hơn.
Nhìn chung, mỗi khi bạn định viết Effect, hãy kiểm tra xem đoạn logic dùng Effect đó có thể tách ra riêng một custom Hook như useData
ở trên không. Càng ít viết useEffect
trực tiếp trong component, app của bạn càng dễ để maintain.
Tổng kết
- Nếu có thể tính toán trong ngay trong render, bạn không cần đến Effect.
- Để cache các tính toán tốn kém, dùng
useMemo
thay vìuseEffect
. - Để reset toàn bộ state của một component, thay đổi
key
của nó. - Để thay đổi một vài state theo prop, hãy thay đổi ngay khi render.
- Code cần chạy vì một component phải xuất hiện trên màn hình thì nên đặt trong Effect, còn lại nên đặt trong event handler.
- Nếu cần cập nhật state của nhiều component cùng lúc, tốt hơn cả là gom hết vào một event.
- Bất cứ lúc nào cố gắng đồng bộ state giữa nhiều component, cân nhắc đẩy state lên trên.
- Bạn có thể kéo về dữ liệu với Effect, nhưng nhớ dọn dẹp để tránh race condition.
Tài liệu tham khảo
Reactjs đang viết lại trang tài liệu chính thức. Một vài điểm đáng chú ý về nội dung mới:
- Tất cả diễn giải sử dụng Hook thay cho class.
- Tăng graphic trực quan và ví dụ tương tác.
- Có câu hỏi (kèm lời giải!) để kiểm tra mức độ hiểu bài của độc giả.
Ở thời điểm năm 2022, bản beta của tài liệu mới tại đây.
Bài viết này dựa phần lớn trên bản gốc You Might Not Need an Effect.