useEffect là gì
useEffect giúp "quản lý vòng đời" cho các functional component, nó cơ bản là sự kết hợp của componentDidMount
, componentDidUpdate
và componentWillUnmount
.
useEffect cũng là nơi bạn quản lý các side-effect hiệu quả.
useEffect thường được dùng để lấy dữ liệu từ API với fetch và gán dữ liệu đó vào local state của component với hàm update của useState
trong Class Component
componentDidMount
: được trigger khi Component đã được hiển thị lần đầu tiên
componentDidUpdate
: được trigger mỗi khi Component đã re-update lại 1 thành phần nào đó trên màn hình
componentWillUnmount
: được trigger trước khi Component đó bị xóa khỏi màn hình, cụ thể nhất chính là chuyển trang
cú pháp
useEffect((input: type) => { /*logic code*/ }, [Array<any>]);
đối số thứ 1 của useEffect là 1 callBack function () => {}
.
hàm callBack này có thể có return hoặc không, return cũng có thể [return;]
hoặc là [return () => {}]
chú ý, hàm callBack này được thực hiện trước khi DOM được sinh ra và sau khi các state được khởi tạo
chú ý, màn callBack được nhắc đến ở đây là đối số 1, không phải phần return của đối số 1
phần return của đối số 1
thời điểm [return () => {}]
được execute có sự khác nhau phụ thuộc vào đối số thứ 2 của Effect, cụ thể:
- đối số thứ 2 không phải là array:
Hay cụ thể hơn là không khai báo gì cho đối số thứ 2. Trường hợp này Effect sẽ luôn được gọi mỗi lần render và reRender.
Khi reRender, hàm[return () => {}]
được execute, nhưng chú ý hàm này đã được build từ lúc ban đầu rồi, param có thể sẽ không được cập nhật
Ví dụ: render cócount = 0
, lúc reRender cócount = 1
, nhưng giá trị trongcallBack vẫn sẽ nhậncount = 0
. - đối số thứ 2 là 1 mảng trống []
hàm[return () => {}]
sẽ được build từ lúc hiển thị ban đầu, nhưng sẽ được execute lúc mà component đó không còn được hiển thị nữa, vd: chuyển trang - đối số thứ 2 là 1 mảng có giá trị
mỗi lần có giá trị trong mảng bị thay đổi, hàm callBack sẽ được execute
người ta gọi hàm callBack này là "cleanup callback function". hay nói cách khác là dọn dẹp lần gọi thứ nhất trước khi gọi lần thứ 2
đối số thứ 2
đối số thứ 2 này là cách để quản lý tần suất hoạt động mặc định của useEffect cho mỗi chu kì render.
nếu không có đối số thứ 2, nghĩa là cú pháp trông như thế này:
useEffect((input: type) => { /*logic code*/ });
nó có nghĩa là hàm này sẽ luôn chạy mỗi lần khi "component mount" và cả khi "component update".
phần "cleanup callback function" sẽ được gọi tại trước lần component update tiếp theo, và lần cuối vào lúc "component unmount".
sử dụng cách code này cần chú ý tránh rơi vào [infinite loop] cho việc re-render liên tục gây ra
nếu đối số thứ 2 này chỉ là mảng rỗng:
useEffect((input: type) => { /*logic code*/ }, []);
hàm sẽ chỉ chạy 1 lần duy nhất khi component mount
phần "cleanup callback function" sẽ được gọi 1 lần vào lúc component unmount
nếu đối số thứ 2 này là mảng có phần tử:
useEffect((input: type) => { /*logic code*/ }, [Array<any>]);
khi mỗi phần tử bên trong Array thay đổi giá trị, hàm sẽ được chạy lại 1 lần.
phần "cleanup callback function" sẽ được gọi tại trước lần component update tiếp theo, và lần cuối vào lúc component unmount
tuy ít xảy ra hơn, nhưng cũng chú ý về [infinite loop] do các phụ thuộc bên trong mảng
- qua đây có thể coi, useEffect có 1 vòng đời, tại 1 thời điểm chỉ có 1 instance của Effect đó được thực thi,
- nếu [Effect đó bị ghi đè bởi chính nó] hoặc [component bị destroy] thì [return () =>{}] mới được thực thi,
- các giá trị bên trong Effect sẽ được lưu lại cho đến khi [return () =>{}] được gọi
CHÚ Ý:
- nếu bạn sử dụng hàm [setState] bên trong hàm [useEffect], giá trị của [state] sẽ không được cập nhật ngay lập tức, nếu bạn sử dụng [state] ngay sau [setState] thì nó sẽ trả ra giá trị cũ
- eslint (extention check lỗi cú pháp trong reactjs) sẽ tự động điền các input mà hàm sử dụng vào trong
[Array<any>]
, hãy chú ý việc này khi làm dự án sử dụng eslint
ví dụ:
//Effect chạy phụ thuộc vào 1 mảng có phần tử
useEffect(() => { alert('count: ' + count); return () => {alert('callBack count: ' + count)};
}, [count]); //Effect luôn chạy mỗi khi có render/re-render
useEffect(() => { alert('always: ' + count); return () => {alert('callBack always: ' + count)};
}); //Effect chỉ chạy 1 lần, cleanup callback function cũng chỉ chạy 1 lần
useEffect(() => { alert('init: ' + count); return () => {alert('callBack init: ' + count)};
}, []);
plugin React Hooks ESLint
Có một plugin ESLint tiện dụng hỗ trợ bạn tuân theo các quy tắc của Hooks. Nó cho bạn biết nếu bạn vi phạm một trong các quy tắc.
eslint-plugin-react-hooks: npm i eslint-plugin-react-hooks
Bạn có thể quên thêm các phần tử phụ thuộc vào danh sách; điều này không phải lúc nào cũng rõ ràng lúc đầu. Bên cạnh đó, những sai sót bất cẩn có thể xảy ra bất cứ lúc nào. trong một số trường hợp, rất có thể bạn sẽ gặp lỗi nếu bỏ qua phần tử phụ thuộc, hãy lưu ý rằng plugin không phải là toàn bộ, chắc chắn có những trường hợp plugin không thể hỗ trợ bạn. Nhưng nếu bạn không hiểu tại sao plugin lại muốn bạn thêm một phần tử phụ thuộc nào đó, thì bạn đừng bỏ qua nó! Ít nhất bạn nên giải thích được tại sao mình không nên thêm nó vào.
Các các phần tử phụ thuộc nên được thêm là gì?
React muốn bạn coi mọi giá trị được sử dụng bên trong Effect là Dynamic - hay nói cách khác, tất cả giá trị có sử dụng trong Effect thì nên được cho vào danh sách phụ thuộc.
Vì vậy, ngay cả khi bạn sử dụng một giá trị bên trong Effect và bạn khá chắc chắn rằng giá trị này không có khả năng thay đổi, hãy cứ thêm nó vào danh sách phụ thuộc.
Đó là bởi vì - tuy nhiên nó có thể không chắc - vẫn có khả năng giá trị sẽ thay đổi. Ai biết liệu Component trong tương lai có thay đổi? Đột nhiên giá trị trở nên có thể thay đổi và bạn đã không thêm nó vào danh sách phụ thuộc, còn nếu không thay đổi thật thì thêm vào cũng đâu có làm code chậm đi đâu?
Do đó, hãy đảm bảo thêm mọi giá trị cần thiết vào danh sách phụ thuộc vì bạn nên coi mọi giá trị là có thể thay đổi. Hãy nhớ rằng nếu ít nhất một thụ thuộc trong mảng khác với lần render trước đó, Effect sẽ được chạy lại.
sử dụng cleanup functions
cùng xét ví dụ này:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(function () { setCount((prev) => prev + 1); }, 1000); }, []); return <p>and the counter counts {count}</p>;
}
function EffectsDemoUnmount() { const [unmount, setUnmount] = useState(false); const renderDemo = () => !unmount && <Counter />; return ( <div> <button onClick={() => setUnmount(true)}>Unmount child component</button> {renderDemo()} </div> );
}
Đoạn code này thực hiện một React component làm cho một bộ đếm tăng một số mỗi giây. Component Cha hiển thị bộ đếm và cho phép bạn Kill bộ đếm bằng cách click vào một button.
Component Con đã đăng ký một interval gọi một hàm mỗi giây. Tuy nhiên, Component đã bị phá hủy mà không hủy đăng ký interval.
Sau khi Component Con bị Kill, interval vẫn hoạt động và nó update biến state bên trong Component ( count), biến này không còn tồn tại.
Giải pháp là clearInterval ngay trước khi phá hủy bộ đếm. Điều này có thể thực hiện được với cleanup function.
Do đó, bạn phải return một callBack function bên trong tham số 1 của effect.
Mình muốn nhấn mạnh rằng các cleanup function không chỉ được gọi trước khi React component unmount. Cleanup function của một Effect được gọi mọi lúc, ngay trước khi thực hiện Effect lần tiếp theo.
The clean-up function runs before the component is removed from the UI to prevent memory leaks. Additionally, if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect. In our example, this means a new subscription is created on every update. To avoid firing an effect on every update, refer to the next section.
useEffect bên trong custom Hooks
cách chi tiết để tạo 1 custom hook không được nhắc đến ở đây, nhưng code của nó sẽ trông như thế này:
const useFetch = (url, initialValue) => { const [data, setData] = useState(initialValue); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async function () { try { setLoading(true); const response = await axios.get(url); if (response.status === 200) { setData(response.data); } } catch (error) { throw error; } finally { setLoading(false); } }; fetchData(); }, [url]); return { loading, data };
};
cách sử dụng hook:
function EffectsDemoCustomHook() { const { loading, data } = useFetch("https://jsonplaceholder.typicode.com/posts/"); return ( <div className="App"> {loading && <div className="loader" />} {data?.length > 0 && data.map((blog) => <p key={blog.id}>{blog.title}</p>)} </div> );
}
theo ví dụ này hàm fetchData được tạo bên trong Effect vì nó chỉ được sử dụng ở đó, đưa ra ngoài là không cần thiết
Như đã đề cập ở trên, có khả năng giá trị sẽ thay đổi trong thời gian chạy trong tương lai, nên phụ thuộc [url] cũng là cần thiết
Nhân tiện, nếu bạn di chuyển các khởi tạo hàm vào các trong các Effect, nó sẽ làm cho code dễ đọc hơn, vì nó chỉ rõ ràng các giá trị phạm vi nào được sử dụng trong Efect. Code thậm chí còn mạnh mẽ hơn.
ngoài ra đưa theo cách này sẽ giúp ESLint không “nhìn thấy” hàm fetchData, ta không phải thêm nó vào danh sách phụ thuộc
sử dụng các hàm không đồng bộ bên trong useEffect
tại sao không thử đưa nguyên hàm async vào trong Effect, hoặc tạo async cho tham số thứ 1 luôn?
rất tiếc, có lỗi xảy ra, Plugin ESLint cũng sẽ cảnh báo về đoạn code đó.
Lý do là đoạn code này trả về một Promise, nhưng một Effect thì chỉ có thể trả về void hoặc một cleanup function thôi.
bên là hãy cứ code như phần [useEffect bên trong custom Hooks] đi
Tổng kết:
- useEffect dùng để hiển thị các Effect hiệu quả
- có 2 tham số, tham số đầu là callback function, nhưng không chấp nhận async function, function này có 1 cleanup function bên trong, chú ý giá trị mà cleanup function nhận được
- tham số 2 là depend list, callback sẽ được gọi lại nếu giá trị trong depend list thay đổi, chú ý eslint với list này, nó có thể làm effect chạy sai mong muốn