useEffect là một hook React cho phép bạn thực hiện các hiệu ứng phụ trong component hàm, thay thế các phương thức vòng đời của component lớp: componentDidMount, componentDidUpdate và componentWillUnmount. Theo React Documentation (2025), useEffect chạy sau khi React đã commit các thay đổi vào DOM, đảm bảo quyền truy cập vào cây DOM thực tế. Hook này nhận một hàm hiệu ứng và một mảng phụ thuộc tùy chọn kiểm soát tần suất thực thi.
Những điểm chính
useEffect là một hook được thêm vào React 16.8 để thực hiện các hiệu ứng phụ trong component hàm. Hiệu ứng phụ là các thao tác không liên quan trực tiếp đến việc render UI: yêu cầu HTTP tới API, đăng ký sự kiện, làm việc với timer, thao tác DOM, ghi log và tích hợp với thư viện bên thứ ba.
Trước khi có hook, tất cả các thao tác này phải được đặt trong các phương thức vòng đời của component lớp: componentDidMount để khởi tạo, componentDidUpdate để phản ứng với thay đổi props, componentWillUnmount để dọn dẹp. useEffect đã hợp nhất ba kịch bản này thành một API duy nhất, nơi mảng phụ thuộc xác định khi nào hiệu ứng nên chạy. Điều này đơn giản hóa logic và giảm trùng lặp mã, đặc biệt trong các kịch bản đăng ký.
Theo React DevTools Usage Survey (2024), useEffect là hook phổ biến thứ hai sau useState, được sử dụng trong 89% ứng dụng React. Hầu hết các nhà phát triển sử dụng nó để lấy dữ liệu, đồng bộ hóa với hệ thống bên ngoài và quản lý đăng ký sự kiện DOM.
import { useEffect } from 'react';
function UserProfile({ userId }) {
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
}
useEffect chạy hàm hiệu ứng được truyền vào sau khi React đã hoàn tất việc render và cập nhật DOM. Đây là điểm khác biệt chính so với các tính toán trong thời gian render: hiệu ứng không chặn việc render, điều này rất quan trọng cho hiệu suất UX. Nếu hiệu ứng chạy đồng bộ, người dùng sẽ thấy giao diện bị đóng băng trong khi tải dữ liệu.
Vòng đời của một hiệu ứng điển hình bao gồm ba giai đoạn. Khi gắn kết component, React thực thi hiệu ứng. Ở mỗi lần cập nhật, nếu ít nhất một phụ thuộc trong mảng đã thay đổi, React trước tiên chạy hàm dọn dẹp của hiệu ứng trước đó, sau đó chạy hiệu ứng mới. Khi tháo gỡ component, chỉ hàm dọn dẹp được chạy.
Theo React Team — useEffect RFC (2024), việc triển khai nội bộ của useEffect sử dụng hàng đợi hiệu ứng phụ trong cây fiber. Sau khi commit các thay đổi (commit phase), React duyệt qua hàng đợi này và gọi các hàm hiệu ứng theo thứ tự chúng được khai báo trong component. Mỗi nút fiber lưu trữ một tham chiếu đến hiệu ứng trước đó để dọn dẹp và chạy lại chính xác.
| Giai đoạn | Hành động của React | Khi nào thực thi |
|---|---|---|
| Gắn kết | Gọi hàm hiệu ứng | Sau lần render đầu tiên |
| Cập nhật | dọn dẹp → hiệu ứng | Khi phụ thuộc thay đổi |
| Tháo gỡ | Chỉ dọn dẹp | Khi component bị xóa |
Mảng phụ thuộc — đối số thứ hai của useEffect — xác định khi nào hiệu ứng nên chạy lại. React so sánh từng giá trị trong mảng với lần render trước đó bằng Object.is. Nếu ít nhất một giá trị đã thay đổi, hiệu ứng chạy lại. Nếu mảng rỗng ([]), hiệu ứng chỉ chạy một lần sau khi gắn kết.
Chọn đúng phụ thuộc là phần khó nhất khi làm việc với useEffect. Mảng phải bao gồm tất cả các biến và hàm được sử dụng bên trong hiệu ứng có thể thay đổi giữa các lần render. Bỏ qua một phụ thuộc dẫn đến stale closure — hiệu ứng nhìn thấy giá trị cũ từ lần render trước. Bao gồm các phụ thuộc không cần thiết gây ra chạy lại quá mức và các lỗi tiềm ẩn.
// Các phụ thuộc kiểm soát khi hiệu ứng chạy lại
useEffect(() => {
document.title = `User: ${user.name}`;
}, [user.name]); // chỉ chạy lại khi user.name thay đổi
// eslint-disable-next-line react-hooks/exhaustive-deps
// Nếu bạn bỏ qua một phụ thuộc, bạn sẽ nhận được dữ liệu cũ
React cung cấp eslint-plugin-react-hooks với quy tắc exhaustive-deps, tự động kiểm tra tính đầy đủ của mảng phụ thuộc. Theo Meta Engineering Blog (2024), việc kích hoạt plugin này giảm 72% lỗi liên quan đến hook. Nên sửa tất cả các cảnh báo exhaustive-deps thay vì triệt tiêu chúng bằng comment, ngoại trừ các trường hợp hiếm có logic tùy chỉnh.
Nếu bạn không truyền mảng phụ thuộc nào, useEffect sẽ chạy sau mỗi lần render. Điều này có thể hữu ích cho việc đồng bộ hóa DOM hoặc ghi log, nhưng thường là một lỗi: hiệu ứng chạy quá thường xuyên, dẫn đến giảm hiệu suất. Trong hầu hết các trường hợp, bạn nên truyền một mảng rỗng (một lần khi gắn kết) hoặc mảng với props/state cụ thể.
Mảng rỗng ([]) có nghĩa là hiệu ứng không phụ thuộc vào bất kỳ giá trị nào và chạy đúng một lần. Điều này tương đương với componentDidMount trong component lớp. Tuy nhiên, cần nhớ: nếu hiệu ứng sử dụng props hoặc state không được liệt kê trong mảng phụ thuộc, hiệu ứng sẽ sử dụng giá trị ban đầu của chúng và không bao giờ thấy các bản cập nhật. Điều này được gọi là stale capture và thường là nguồn gốc của các lỗi khó tìm.
| Mảng phụ thuộc | Hành vi | Tương đương lớp |
|---|---|---|
| Không đối số | Sau mỗi lần render | componentDidUpdate |
| [] | Một lần khi gắn kết | componentDidMount |
| [a, b] | Khi a hoặc b thay đổi | Tương tự ComponentWillReceiveProps |
| return cleanup | Quản lý tháo gỡ | componentWillUnmount |
Hàm dọn dẹp là một hàm mà useEffect có thể trả về từ callback của nó. React gọi nó khi tháo gỡ component và trước khi chạy lại hiệu ứng khi phụ thuộc thay đổi. Dọn dẹp cần thiết để hủy đăng ký, timer, yêu cầu và bất kỳ tài nguyên nào cần được giải phóng.
Một ví dụ điển hình là đăng ký WebSocket. Khi gắn kết, một kết nối được tạo; khi cập nhật phụ thuộc, nó được tạo lại (dọn dẹp đóng kết nối cũ, hiệu ứng mở kết nối mới); khi tháo gỡ, nó bị đóng. Nếu không có dọn dẹp, mỗi lần gắn kết lại component sẽ tạo một kết nối WebSocket mới, dẫn đến rò rỉ bộ nhớ và nhiều kết nối.
useEffect(() => {
const socket = new WebSocket('wss://api.example.com');
socket.onmessage = event => setData(event.data);
// Hàm dọn dẹp — chạy khi tháo gỡ và trước khi chạy lại
return () => {
socket.close();
};
}, []);
Theo React Documentation (2025), AbortController là cách tiếp cận hiện đại để hủy các yêu cầu fetch trong dọn dẹp. Nếu hiệu ứng thực hiện yêu cầu HTTP và component bị tháo gỡ trước khi hoàn thành, yêu cầu tiếp tục chạy và setState sau khi tháo gỡ gây ra lỗi. Tạo AbortController bên trong hiệu ứng và gọi controller.abort() trong dọn dẹp để hủy yêu cầu.
Lỗi phổ biến nhất là bỏ qua phụ thuộc. Ví dụ, hiệu ứng sử dụng prop userId, nhưng mảng phụ thuộc lại rỗng. Kết quả là, hiệu ứng chạy một lần với giá trị userId ban đầu và không bao giờ phản ứng với các thay đổi của nó. Nhà phát triển thấy component nhận được userId mới, nhưng dữ liệu không được cập nhật. eslint-plugin-react-hooks với quy tắc exhaustive-deps tự động phát hiện các lỗi như vậy.
// ❌ Race condition — không hủy bỏ
useEffect(() => {
fetch(`/api/user/${userId}`).then(res => setUser(res));
}, [userId]);
// ✅ Đã sửa với AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => setUser(res));
return () => controller.abort();
}, [userId]);
Để giải quyết vấn đề vòng lặp vô hạn, tránh đặt logic trong useEffect để cập nhật state dựa trên state trước đó. Sử dụng dạng hàm của setState hoặc di chuyển các tính toán ra ngoài hiệu ứng. Nếu hiệu ứng đăng ký sự kiện storage hoặc trình duyệt, hãy đảm bảo phiên bản listener được tạo một lần, không phải mỗi lần render.
Câu hỏi thường gặp
Trực tiếp — không, vì useEffect mong đợi trả về một hàm đồng bộ hoặc undefined. Nếu callback được khai báo là async, nó trả về một Promise mà React bỏ qua và cơ chế dọn dẹp ngừng hoạt động. Giải pháp: gọi một hàm async bên trong hiệu ứng: useEffect(() => { async function load() { ... }; load(); }, []).
Không có giới hạn. React khuyến nghị tách logic không liên quan thành các useEffect riêng biệt, ngay cả khi chúng có cùng mảng phụ thuộc. Mỗi hiệu ứng nên chịu trách nhiệm cho một tác vụ phụ được xác định rõ ràng: một cho đăng ký, một cho tải dữ liệu, một cho đồng bộ hóa tiêu đề tab. Điều này giúp đơn giản hóa việc hiểu và gỡ lỗi.
Trong React Strict Mode (chỉ chế độ phát triển), tất cả các hiệu ứng được gắn kết, tháo gỡ và gắn kết lại. Đây là một tính năng, không phải lỗi — React kiểm tra xem dọn dẹp có hoạt động chính xác không. Nếu sau khi tháo gỡ và gắn kết lại hiệu ứng hoạt động không đúng (ví dụ: đăng ký trùng lặp), thì dọn dẹp của bạn chưa hoàn chỉnh. Trong môi trường production, hiệu ứng chạy một lần.
Sử dụng AbortController. Tạo một controller bên trong hiệu ứng, truyền controller.signal vào fetch và gọi controller.abort() trong dọn dẹp. Nếu component bị tháo gỡ trước khi yêu cầu hoàn thành, fetch bị hủy và setState sẽ không được gọi. Điều này ngăn chặn race condition và lỗi “Can't perform a React state update on an unmounted component”.
useEffect sẽ chạy sau mỗi lần render không ngoại lệ. Điều này có nghĩa là bất kỳ setState nào bên trong hiệu ứng sẽ gây ra render mới → hiệu ứng mới → vòng lặp vô hạn. Trong thực tế, một hiệu ứng không có mảng phụ thuộc hầu như luôn là một lỗi. Ngoại lệ là ghi log hoặc đồng bộ hóa với hệ thống bên ngoài nơi mỗi lần render yêu cầu đồng bộ hóa.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm