useEffect
là một hook trong ReactJS cho phép bạn thực hiện các tác vụ phụ thuộc vào lifecycle của component. Nó giúp bạn tương tác với DOM, gọi API, đăng ký/sẵn sàng các sự kiện, và thực hiện các tác vụ không đồng bộ khác.
Các khái niệm, ý nghĩa và cú pháp của useEffect đã quá quen thuộc với các anh em dev ReactJs nên ở bài viết này mình sẽ chỉ đề cập đến các vấn đề thường gặp phải khi dùng hook này và các lưu ý để sử dụng useEffect hiệu quả hơn.
Nào cùng vào vấn đề ngay thôi!!
1. Vòng lặp vô tận
useEffect
sẽ được gọi mỗi khi component render lại. Nếu không chú ý, bạn có thể gặp phải vòng lặp vô tận nếu gọi một hàm trong useEffect
mà thay đổi state và dẫn đến việc render component liên tục. Để tránh vấn đề này, bạn cần chỉ định một mảng dependencies (dependencies array) là tham số thứ hai của useEffect
và đảm bảo rằng các dependencies không thay đổi trong useEffect
Dưới đây là một ví dụ về vòng lặp vô tận có thể xảy ra khi sử dụng useEffect
mà không xử lý đúng dependencies:
import React, { useEffect, useState } from 'react';
const InfiniteLoopExample = () => {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Thay đổi state count trong useEffect
});
return (
<div>
<p>Count: {count}</p>
</div>
);
};
export default InfiniteLoopExample;
Trong ví dụ trên, useEffect
không có dependencies được chỉ định. Mỗi khi component render lại, useEffect
sẽ được gọi và gọi setCount
để tăng giá trị của count
lên 1. Tuy nhiên, điều này dẫn đến một vòng lặp vô tận, vì mỗi lần setCount
được gọi, nó sẽ làm cho component render lại, và useEffect
sẽ lại được gọi và thay đổi count
một lần nữa, và quá trình này lặp đi lặp lại mà không bao giờ kết thúc.
Để khắc phục vòng lặp vô tận này, bạn cần chỉ định dependencies cho useEffect
, đảm bảo rằng setCount
chỉ được gọi khi count
thay đổi. Bạn có thể chỉ định count
là dependencies bằng cách truyền một mảng dependencies vào useEffect
:
useEffect(() => {
setCount(count + 1);
}, [count]); // Chỉ định dependencies là count
Khi bạn chỉ định count
là dependencies, useEffect
chỉ được gọi lại khi count
thay đổi. Điều này ngăn chặn vòng lặp vô tận và chỉ thực hiện setCount
khi count
thay đổi, không phải mỗi khi component render lại.
2. Nếu giá trị trong dependencies là một Object
hay Function
thì sao?
Trong JavaScript
, chúng ta biết rằng khi so sánh:
// Primitive values
const prevURL = 'https://haodev.wordpress.com';
const currURL = 'https://haodev.wordpress.com';
prevURL === currURL // TRUE
// Reference values
const prevBlog = { name: 'Make It Awesome' };
const currBlog = { name: 'Make It Awesome' };
prevBlog === currBlog // FALSE
Ở một số trường hợp thực tế, Dependency
có thể là 1 mảng các props
, state
, thậm chí là một function
, như vậy thì sẽ không thể tránh khỏi việc trigger
effects
thừa thãi.
Cùng đi vào chi tiết nhé!
Dependency
chứa object
:
const ObjDependency = () => {
const [vote, setVote] = useState({
value: 0,
});
useEffect(() => {
console.log("Component is invoked when vote changes");
}, [vote]);
return (
<>
<p>Vote value: {vote.value}.</p>
<button onClick={() => setVote({ value: 0 })}>Set vote = 0</button>
</>
);
};
Lúc này, dù vote.value
vẫn bằng 0
nhưng chuỗi Component is invoked when vote changes
vẫn sẽ được log
ra khi ta click
vào button
. Lúc này, dù vote.value
vẫn bằng 0
nhưng chuỗi Component is invoked when vote changes
vẫn sẽ được log
ra khi ta click
vào button
Để xem nào, chúng ta sẽ có một vài hướng tiếp cận để xử lý như sau:
- Chỉ thêm những giá trị
property
thật sự cần thiết
useEffect(() => {
console.log("Component is invoked when vote.value changes");
}, [vote.value]);
Truyền vote.value
vào mảng dependency
thay vì đưa cả object
vote
vào như trước.
Song, chẳng phải lúc nào value
cũng tồn tại trong vote
(optional property) hoặc nếu object
đó có nhiều properties
thì ta phải liệt kê hết vào hay sao? 🙃🙃))
Đến đây thì có thể tham khảo 03 cách tiếp theo:
- Sử dụng
JSON.stringify()
useEffect(() => {
console.log("Component is invoked when JSON.stringify(vote) changes");
}, [JSON.stringify(vote)]);
- Kết hợp
useRef()
and một sốhelpers
hỗ trợ so sánhobject
const useDeepCompareWithRef = (value) => {
const ref = useRef();
// Hoặc 1 helper deep comparison 2 objects thay vì lodash _.isEqual()
if (!_.isEqual(value, ref.current)) {
ref.current = value;
}
return ref.current;
};
useEffect(() => {
console.log("Component is invoked when vote changes with useDeepCompareWithRef()");
}, [useDeepCompareWithRef(vote)]);
- Dùng
use-deep-compare-effect
Tới đây, nếu object
của chúng ta vẫn quá phức tạp để so sánh (thông thường thì không tới mức đó, hoặc chỉ đơn giản là bạn muốn kế thừa open source
sẵn có 😸😸) thì có thể tham khảo package này
.
Chỉ cần thay useEffect()
bằng useDeepCompareEffect()
là mọi thứ ổn thỏa, chẳng cần "xoắn quẩy"
nữa:
import useDeepCompareEffect from 'use-deep-compare-effect';
useDeepCompareEffect(() => {
console.log("Component is invoked when vote changes with useDeepCompareEffect()");
},[obj])
Dependency
chứa function
:
Xét một trường hợp dưới đây:
const FuncDependency = ({ data}) => {
const doSomething = () => { console.log(data); };
useEffect(() => {
doSomething();
}, []);
// ...
};
doSomething()
sử dụng props
data
, nhưng data
lại không nằm trong dependency
. Điều này dẫn tới việc khi ai đó cập nhật data
, doSomething()
sẽ không được gọi lại.
Do đó, trong trường hợp này, chúng ta có thể định nghĩa doSomething
bên trong useEffect()
rồi gọi luôn.
Thực tế trong dự án, chúng ta thường tách các requests
, logics
, UIs
, helpers
thành các files
độc lập để thuận tiện cho việc tái sử dụng và viết unit test
hoặc hàm đó là một props
nhận từ component cha
Cùng đi tới một ví dụ nữa, chúng ta có 02 components
: Parent
và Child
:
Parent
là component cha củaChild
Parent
truyền 2 hàmupdateAnyStates
vàupdateCounter
xuốngChild
như đoạn code
dưới đây:
const Parent = () => {
const [counter, setCounter] = useState(0);
const [anotherState, setAnotherState] = useState(0);
const doSTOnAnyChange = () => { console.log("doSTOnAnyChange runs on ANY changes") };
const doSTOnCounterChange = () => { console.log("doSTOnCounterChange should run on COUNTER changes") };
return (
<>
<button onClick={() => setCounter(counter + 1)}>Update counter state</button>
<button onClick={() => setAnotherState(anotherState + 1)}>Update a different state</button>
<Child doSTOnAnyChange={doSTOnAnyChange} doSTOnCounterChange={doSTOnCounterChange} />
</>
);
}
const Child = ({ doSTOnAnyChange, doSTOnCounterChange }) => {
useEffect(() => {
doSTOnAnyChange();
}, [doSTOnAnyChange]);
useEffect(() => {
doSTOnCounterChange();
}, [doSTOnCounterChange]);
return <p>Child</p>;
}
Luôn có 2 chuỗi:
> doSTOnAnyChange runs on ANY changes
> doSTOnCounterChange should run on COUNTER changes
được log
dưới cửa sổ Console
, điều này nghĩa là, khi Parent
re-renders
, Child
nhận thấy sự thay đổi của cả updateAnyStates
, updateCounter
.
Thực tế thì hàm doSTOnCounterChange
– đúng như tên gọi của nó – chỉ cần chạy lại khi có sự thay đổi của state couter
thôi.
Tới đây thì useCallback()
được sinh ra cho “đời bớt khổ”, hạn chế những lần chạy không cần thiết 😸😸
Chỉ cần thay đổi một chút khi khai báo hàm doSTOnCounterChange
:
const doSTOnCounterChange = useCallback(() => {
console.log("doSTOnCounterChange should run on COUNTER changes");
}, [counter]);
Với useCallback()
, chẳng cần phải lo nghĩ doSTOnCounterChange
bị tạo lại mỗi lần Parent
re-renders
nữa.
Như vậy là chúng ta đã cùng nhau điểm qua cơ chế hoạt động của useEffect()
và một số trường hợp thú vị xung quanh nó rồi.
Hy vọng rằng bài viết này có thể giúp ích được các bạn đang tiếp cận với ReactJS
, từ đó có thể hiểu về luồng của ứng dụng và kiểm soát được một số lỗi liên quan tốt hơn.