useEffect trong React: side effect, dependency array và cleanup

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Component React chủ yếu mô tả giao diện, nhưng ứng dụng thật còn cần làm những việc bên ngoài thế giới của React: gọi API, đặt timer, đăng ký sự kiện, cập nhật tiêu đề trang. Những việc đó gọi là side effect, và useEffect là nơi bạn đặt chúng.

Cú pháp cơ bản

import { useEffect, useState } from 'react';

function TieuDeTrang() {
  const [dem, setDem] = useState(0);

  useEffect(() => {
    document.title = 'Bạn đã bấm ' + dem + ' lần';
  }, [dem]);

  return <button onClick={() => setDem(dem + 1)}>Bấm ({dem})</button>;
}

Hàm truyền vào chạy sau khi React cập nhật giao diện. Mảng thứ hai gọi là dependency array, quyết định khi nào effect chạy lại.

Ba dạng dependency array

  • Không truyền mảng: effect chạy sau mỗi lần render. Hiếm khi bạn muốn vậy.
  • Mảng rỗng []: chạy một lần sau lần render đầu tiên, thích hợp cho khởi tạo.
  • Mảng có giá trị [a, b]: chạy sau lần đầu và mỗi khi a hoặc b thay đổi.

Quy tắc vàng: mọi giá trị từ component (props, state, biến, hàm) mà effect sử dụng phải có trong mảng. Công cụ ESLint của React (plugin react-hooks) sẽ cảnh báo nếu bạn thiếu, hãy nghe theo nó.

Cleanup: dọn dẹp khi kết thúc

Effect có thể trả về một hàm dọn dẹp. React gọi nó trước khi chạy lại effect và khi component bị gỡ khỏi màn hình:

function DongHo() {
  const [gio, setGio] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setGio(new Date()), 1000);
    return () => clearInterval(id);   // dọn dẹp
  }, []);

  return <p>{gio.toLocaleTimeString('vi-VN')}</p>;
}

Nếu quên cleanup, timer vẫn chạy sau khi component biến mất, gây rò rỉ bộ nhớ và lỗi cập nhật state trên component đã gỡ. Tương tự với addEventListener cần removeEventListener.

Lấy dữ liệu từ API

function DanhSachBai({ chuDe }) {
  const [bai, setBai] = useState([]);
  const [loi, setLoi] = useState(null);
  const [dangTai, setDangTai] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    setDangTai(true);
    fetch('/api/bai-viet?chuDe=' + encodeURIComponent(chuDe), {
      signal: controller.signal,
    })
      .then(r => {
        if (!r.ok) throw new Error('Lỗi ' + r.status);
        return r.json();
      })
      .then(setBai)
      .catch(e => { if (e.name !== 'AbortError') setLoi(e.message); })
      .finally(() => setDangTai(false));

    return () => controller.abort();
  }, [chuDe]);

  if (dangTai) return <p>Đang tải...</p>;
  if (loi) return <p>{loi}</p>;
  return <ul>{bai.map(b => <li key={b.id}>{b.tieuDe}</li>)}</ul>;
}

Khi chuDe đổi, cleanup hủy yêu cầu cũ rồi gọi yêu cầu mới, tránh tình trạng kết quả cũ về muộn ghi đè kết quả mới. Trong dự án lớn, nhiều đội dùng thư viện lấy dữ liệu như TanStack Query hay SWR để xử lý chuyện này gọn hơn.

Những cạm bẫy phổ biến

  1. Vòng lặp vô hạn: effect cập nhật state mà state đó lại nằm trong dependency, khiến effect chạy mãi. Xem lại xem có cần effect không.
  2. Object và hàm tạo mới mỗi lần render: đưa chúng vào dependency làm effect chạy lại liên tục. Hãy đưa chúng vào bên trong effect, hoặc dùng useMemo, useCallback khi thực sự cần.
  3. Dùng effect cho thứ không phải side effect: nếu chỉ cần tính toán giá trị từ props hay state, hãy tính ngay trong lúc render, không cần effect và state phụ.
  4. Effect chạy hai lần khi phát triển: ở chế độ Strict Mode trong môi trường phát triển, React cố ý chạy effect, cleanup rồi chạy lại để giúp bạn phát hiện cleanup thiếu. Đây không phải lỗi.
  5. Dùng async trực tiếp làm hàm effect: hàm effect không được trả về promise. Hãy khai báo hàm async bên trong rồi gọi nó.

Câu hỏi tự kiểm tra: có thực sự cần effect?

  • Phản hồi một sự kiện của người dùng? Hãy xử lý trong trình xử lý sự kiện.
  • Tính dữ liệu từ props hoặc state? Tính trong lúc render.
  • Đồng bộ với hệ thống bên ngoài (API, DOM, timer, đăng ký)? Đây mới là chỗ dùng effect.

Ví dụ: lắng nghe kích thước cửa sổ

Một ví dụ khác về đăng ký và dọn dẹp sự kiện là theo dõi chiều rộng cửa sổ:

function useChieuRong() {
  const [rong, setRong] = useState(window.innerWidth);

  useEffect(() => {
    const xuLy = () => setRong(window.innerWidth);
    window.addEventListener('resize', xuLy);
    return () => window.removeEventListener('resize', xuLy);
  }, []);

  return rong;
}

Hàm cleanup gỡ đúng trình lắng nghe đã đăng ký, vì dùng cùng tham chiếu hàm xuLy. Đây cũng là một custom hook đơn giản: bạn có thể dùng nó ở bất kỳ component nào bằng const rong = useChieuRong() mà không lặp lại logic.

Kết luận

useEffect là cầu nối giữa component và thế giới bên ngoài. Hãy nhớ khai báo đủ dependency, luôn dọn dẹp những gì mình đăng ký và chỉ dùng effect khi thật sự cần đồng bộ với bên ngoài.

Để thực hành useEffect qua dự án có gọi API thật, bạn có thể tham khảo khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment