useEffect: nó là gì, hook hiệu ứng phụ và vòng đời trong React

Tác giả: IT Sectr Đã đăng: 2026-07-04 Thời gian đọc: 9 phút

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à hook để thực hiện các hiệu ứng phụ sau khi render component.
  • Mảng phụ thuộc kiểm soát khi hiệu ứng chạy lại; mảng rỗng = một lần.
  • Dọn dẹp — hàm dọn dẹp từ hiệu ứng được gọi khi tháo gỡ và trước khi chạy lại.
  • Vòng đời — thay thế componentDidMount, componentDidUpdate và componentWillUnmount.
  • Thứ tự thực thi — hiệu ứng chạy sau khi các thay đổi DOM được commit.

useEffect trong React là gì

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.

jsx
import { useEffect } from 'react';

function UserProfile({ userId }) {
    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => setUser(data));
    }, [userId]);
}

useEffect hoạt động như thế nào: vòng đời hiệu ứng

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ạnHành động của ReactKhi nào thực thi
Gắn kếtGọi hàm hiệu ứngSau lần render đầu tiên
Cập nhậtdọn dẹp → hiệu ứngKhi phụ thuộc thay đổi
Tháo gỡChỉ dọn dẹpKhi component bị xóa

Mảng phụ thuộc của useEffect

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.

jsx
// 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.

useEffect không có phụ thuộc và với mảng rỗng

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ộcHành viTương đương lớp
Không đối sốSau mỗi lần rendercomponentDidUpdate
[]Một lần khi gắn kếtcomponentDidMount
[a, b]Khi a hoặc b thay đổiTương tự ComponentWillReceiveProps
return cleanupQuản lý tháo gỡcomponentWillUnmount

Dọn dẹp hiệu ứng trong useEffect

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.

jsx
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.

Các lỗi thường gặp với useEffect

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.

  • Vòng lặp vô hạn — cập nhật state bên trong hiệu ứng, gây ra render lại, kích hoạt hiệu ứng một lần nữa. Giải pháp: kiểm tra mảng phụ thuộc hoặc sử dụng dạng hàm của setter.
  • Race condition — nếu userId thay đổi nhanh, yêu cầu cho userId đầu tiên có thể hoàn thành sau yêu cầu cho userId thứ hai, hiển thị dữ liệu không chính xác. Giải pháp: sử dụng cờ cancelled hoặc AbortController.
  • Hiệu ứng dư thừa — kết hợp logic không liên quan trong một useEffect. React khuyến nghị chia logic thành nhiều hiệu ứng, ngay cả khi chúng có cùng mảng phụ thuộc.
  • Quên dọn dẹp — không hủy đăng ký sự kiện, dọn dẹp timer hoặc hủy yêu cầu dẫn đến rò rỉ bộ nhớ và lỗi setState sau khi tháo gỡ.
jsx
// ❌ 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

Có thể sử dụng async/await bên trong useEffect không?

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(); }, []).

Có thể có bao nhiêu useEffect trong một component?

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.

Tại sao useEffect chạy hai lần trong StrictMode?

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.

Làm thế nào để hủy yêu cầu fetch trong useEffect?

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”.

Điều gì xảy ra nếu tôi không truyền mảng phụ thuộc?

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

  • useEffect là hook để thực hiện hiệu ứng phụ sau khi commit DOM, thay thế componentDidMount, componentDidUpdate và componentWillUnmount.
  • Mảng phụ thuộc kiểm soát việc chạy lại hiệu ứng; mảng rỗng = một lần, bỏ qua phụ thuộc = stale closure.
  • Dọn dẹp là bắt buộc cho đăng ký, timer và yêu cầu; nếu không có sẽ xảy ra rò rỉ bộ nhớ.
  • AbortController là cách chính xác để hủy yêu cầu fetch trong useEffect, ngăn chặn race condition.
  • StrictMode gắn kết hiệu ứng hai lần trong chế độ dev để xác minh tính đúng đắn của dọn dẹp.
  • Tách hiệu ứng — mỗi useEffect xử lý một tác vụ, ngay cả khi phụ thuộc trùng khớp.
  • eslint-plugin-react-hooks tự động kiểm tra tính đầy đủ của mảng phụ thuộc, giảm 72% lỗi.

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.

Thảo luận dự án

Đọc thêm