useEffect chạy một đoạn code sau khi React đã render component và cập nhật màn hình. Bạn dùng nó để giữ component đồng bộ với thứ nằm ngoài React, như một timer, một kết nối mạng hay một event listener, và bạn quyết định khi nào nó chạy lại bằng một mảng dependency.
Mở console bên dưới bản xem trước: effect ghi log một lần khi component xuất hiện, rồi ghi lại sau mỗi lần click. Đổi [count] thành [] rồi click tiếp: sau log đầu tiên, effect im lặng.
Cú pháp
useEffect(() => {
// setup: runs after the render is on the screen
return () => {
// cleanup: runs before the next setup, and on unmount
};
}, [dependencies]);
- Đối số thứ nhất là hàm setup. Hàm này có thể trả về một hàm cleanup, hoặc không trả về gì.
- Đối số thứ hai là mảng dependency: mọi giá trị trong component mà hàm setup đọc tới (props, state và mọi thứ được tính ra từ chúng).
- Gọi
useEffectở cấp cao nhất của component, giống như mọi hook khác.
Effect chạy sau khi render
Render là việc React gọi hàm component để biết màn hình cần hiển thị gì. Effect đợi đến khi việc đó xong và DOM đã được cập nhật, nên bên trong effect, trang đã hiển thị các giá trị mới.
Mỗi lần click ghi log render trước, effect sau, và effect đọc được đúng chữ trên nút mà React vừa ghi ra. Đó là lý do việc đọc DOM, khởi chạy timer hay kết nối tới máy chủ thuộc về effect chứ không nằm trong thân component: thân component chạy trong lúc render, trước khi có gì trên màn hình, và React có thể gọi nó nhiều lần hơn bạn nghĩ.
Effect thường chạy sau khi trình duyệt đã vẽ. Khi effect cần đo DOM trước lúc vẽ, hãy dùng useLayoutEffect, hook luôn chạy trước khi vẽ.
Mảng dependency: ba dạng
Mảng dependency quyết định khi nào effect chạy lại. Có ba dạng, và ví dụ dưới đây dùng cả ba để bạn thấy sự khác nhau.
Khi tải trang, cả ba effect đều ghi log. Click a + 1 thì hai effect chạy: effect không có mảng và effect phụ thuộc vào a. Click b + 1 thì chỉ effect không có mảng chạy, vì không có gì trong [] hay [a] thay đổi.
| Mảng dependency | Khi nào effect chạy |
|---|---|
| không có | Sau mỗi lần render |
[] | Một lần, sau lần render đầu tiên (khi mount) |
[a, b] | Sau lần render đầu tiên, rồi mỗi khi a hoặc b thay đổi |
React so sánh từng dependency với giá trị trước đó bằng Object.is. Số, chuỗi và boolean được so sánh theo giá trị. Object, mảng và hàm được so sánh theo định danh, điều này rất quan trọng (xem bên dưới).
Đừng bỏ một giá trị ra khỏi mảng để effect chạy ít hơn. Effect sẽ tiếp tục dùng giá trị từ một lần render cũ. Quy tắc lint react-hooks/exhaustive-deps bắt được lỗi này; nó nằm trong cấu hình ESLint mà template React của Vite và cấu hình ESLint của Next.js đã có sẵn.
Cleanup
Nhiều effect khởi động một thứ cần được dừng lại: một kết nối, một subscription, một timer. Trả về một hàm từ effect và React sẽ gọi nó vào hai thời điểm: ngay trước khi effect chạy lại với dependency mới, và khi component rời khỏi màn hình (unmount).
Chọn một phòng khác: console hiện disconnect from general rồi connect to music. Kết nối cũ luôn đóng trước khi kết nối mới mở. Click Hide và cleanup chạy lần cuối, vì ChatRoom đã bị unmount.
Hàm connect ở đây đóng vai một client chat hay WebSocket thật. Với client nào thì cấu trúc cũng vậy: tạo nó trong setup, đóng nó trong cleanup.
Xóa timer
Một timer khởi chạy trong effect sẽ tiếp tục chạy sau khi component đã biến mất, trừ khi cleanup dừng nó lại.
Xóa dòng clearInterval(id);, rồi ẩn timer: tick vẫn tiếp tục xuất hiện trong console từ một component không còn tồn tại. Mỗi lần Show lại khởi chạy thêm một timer nữa.
Chú ý setSeconds((s) => s + 1). Effect chỉ chạy một lần, nên một lệnh setSeconds(seconds + 1) thông thường sẽ mãi đọc seconds từ lần render đầu tiên và dừng ở 1. Hàm updater thì luôn nhận được giá trị mới nhất.
Gỡ event listener
Event listener trên window hay document cũng theo cùng cách này. Truyền cho removeEventListener đúng hàm mà bạn đã truyền cho addEventListener:
useEffect(() => {
function handleKeyDown(e) {
if (e.key === 'Escape') onClose();
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
Vòng lặp vô hạn
Một effect set giá trị state mà chính nó phụ thuộc vào sẽ tạo ra một vòng lặp: state đổi, React render, dependency đã đổi, effect chạy và lại set state.
// Never stops: every render runs the effect, every effect causes a render
useEffect(() => {
setCount(count + 1);
});
// Same loop: count changes, so the effect runs again
useEffect(() => {
setCount(count + 1);
}, [count]);
Component render không dứt và tab luôn bận. React không chặn vòng lặp này: trong môi trường phát triển nó ghi lỗi "Maximum update depth exceeded" ra console, còn ở production thì không ghi gì. Ví dụ dưới đây chạy phiên bản thứ hai với một điều kiện dừng, để bạn xem vòng lặp mà trang không bị treo:
Console ghi từ render 0 đến render 5: sáu lần render cho một lần mount, lần nào cũng do effect khởi phát. Cách sửa tùy vào ý định của bạn:
- Bạn chỉ muốn chạy một lần. Dùng
[]và dạng updater,setCount((c) => c + 1), đểcountkhông phải là dependency. - Bạn đang tính một giá trị từ state khác. Đừng lưu nó; hãy tính trong lúc render, như ở phần cuối của trang này.
- Bạn đang phản hồi một thao tác của người dùng. Đặt code vào event handler.
Object và hàm làm dependency
Một object, mảng hay hàm được tạo trong thân component là một giá trị mới ở mỗi lần render, nên effect phụ thuộc vào nó sẽ chạy sau mỗi lần render, kể cả khi bên trong không có gì thay đổi.
Việc đổi màu chẳng liên quan gì tới tìm kiếm, vậy mà mỗi lần đổi lại ghi search for react. Chuyển dòng options vào bên trong effect và đổi mảng thành [query]: đổi màu không còn ghi log, chỉ việc gõ vào ô nhập mới chạy tìm kiếm.
Đó là cách sửa chung. Tạo object và hàm bên trong effect dùng chúng, và phụ thuộc vào các giá trị nguyên thủy dùng để tạo ra chúng. Khi một hàm buộc phải nằm ngoài effect, useCallback giữ định danh của nó ổn định giữa các lần render.
Vì sao effect chạy hai lần trong môi trường phát triển
Khi bật <StrictMode> (một ứng dụng Vite mới bọc root trong nó, và App Router của Next.js bật nó mặc định), React chạy một phép thử trong môi trường phát triển: mount từng component, chạy các effect, chạy cleanup của chúng, rồi chạy lại các effect. Với phòng chat ở trên, console trong môi trường phát triển hiện:
connect to general
disconnect from general
connect to general
Việc này chỉ xảy ra một lần, khi mount, và chỉ trong môi trường phát triển. Nó tồn tại để lộ ra những effect còn thiếu cleanup: nếu hai lần chạy liên tiếp làm hỏng thứ gì, người dùng cũng sẽ làm hỏng nó khi rời đi rồi quay lại. Cách sửa không bao giờ là chặn lần chạy thứ hai bằng một ref, mà là viết cleanup. Bản xem trước trên trang này chạy như một bản build production, nên bạn sẽ không thấy lần chạy kép ở đây.
Có thể bạn không cần effect
Effect dùng để đồng bộ với các hệ thống nằm ngoài React. Có hai cách dùng phổ biến sẽ tốt hơn khi không có effect.
Giá trị tính ra từ props hoặc state. Hãy tính chúng trong lúc render. Lưu chúng vào state rồi đồng bộ bằng effect sẽ tốn thêm một lần render, và lần render đầu tiên trong số đó đưa một giá trị cũ vào DOM:
Mỗi lần click, WithEffect render hai lần, và lần render đầu vẫn hiện tên cũ. Computed render một lần với giá trị đúng.
Code phản hồi một thao tác của người dùng. Gửi form, hiện thông báo sau khi mua hàng, hay gửi một sự kiện analytics cho một lần click thuộc về event handler. Ở đó bạn biết chính xác điều gì đã xảy ra, còn effect chỉ biết rằng một giá trị nào đó đã đổi.
useEffectEvent
Đôi khi effect cần giá trị mới nhất của một prop mà không chạy lại khi prop đó đổi. Trong phòng chat, thông báo nên dùng theme hiện tại, nhưng đổi theme thì không nên kết nối lại. React 19.2 thêm useEffectEvent cho trường hợp này:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme); // always the latest theme
});
useEffect(() => {
const connection = createConnection(roomId);
connection.on('connected', () => onConnected());
connection.connect();
return () => connection.disconnect();
}, [roomId]); // theme is not a dependency
}
Hàm tạo từ useEffectEvent chỉ dùng để gọi bên trong effect. Đừng truyền nó cho component khác hay liệt kê nó trong mảng dependency.
Câu hỏi thường gặp
useEffect chạy khi nào?
Sau khi React đã render component và cập nhật DOM, thường là sau khi trình duyệt đã vẽ xong. Không có mảng dependency thì nó chạy sau mỗi lần render, với [] thì chỉ sau lần render đầu tiên, còn với [a, b] thì sau lần render đầu tiên và sau mỗi lần render mà a hoặc b thay đổi.
Hàm cleanup của useEffect là gì?
Là hàm bạn trả về từ effect. React gọi nó trước khi chạy lại effect với dependency mới, và thêm một lần nữa khi component bị gỡ khỏi màn hình. Dùng nó để xóa timer, gỡ event listener và đóng subscription.
Vì sao useEffect chạy hai lần?
Trong môi trường phát triển, <StrictMode> mount mọi component, chạy các effect của nó, dọn dẹp chúng rồi chạy lại, để chỉ ra những effect còn thiếu cleanup. Việc này chỉ xảy ra một lần khi mount và chỉ trong môi trường phát triển. Bản build production chạy effect một lần.
Vì sao useEffect gây ra vòng lặp vô hạn?
Effect set một giá trị state cũng nằm trong mảng dependency của chính nó (hoặc không có mảng nào cả). Set state khiến component render lại, dependency đã đổi, nên effect lại chạy. Hãy dùng hàm updater và bỏ dependency đó, thêm một điều kiện, hoặc tính giá trị ngay trong lúc render.
Callback của useEffect có thể là hàm async không?
Không. Callback phải không trả về gì hoặc trả về một hàm cleanup, còn hàm async thì trả về một promise. Hãy khai báo một hàm async bên trong effect rồi gọi nó, như trong trang lấy dữ liệu.