useEffect là hook để đồng bộ component với thế giới bên ngoài, như gọi API, thao tác DOM, subscribe vào event, kết nối WebSocket…
Nhưng nó có một tham số quyết định khi nào nó chạy, và nếu dùng sai thì hậu quả không nhỏ.
Cú pháp tổng quát
useEffect đều xoay quanh hai thứ: có clean-up hay không, và dependency array trông như thế nào.
1. Không có dependency array — chạy sau mỗi lần render
setState.
Khi nào dùng: Debug xem component render bao nhiêu lần, hoặc những tác vụ thực sự cần chạy lại sau mọi render (rất hiếm).
2. Dependency array rỗng [] — chạy một lần lúc mount
componentDidMount trong class component.
Khi nào dùng: Fetch data khởi tạo, đọc localStorage, khởi tạo thư viện bên thứ ba.
localStorage trong useEffect với [] vì đây là tác vụ chỉ cần chạy một lần lúc mount — lấy giá trị đã lưu từ lần trước để khởi tạo state.
Lưu ý: Không sử dụnguseStateđể lấylocalStoragekhi đang ở trong một server-component của Next.js, vìlocalStoragethuộc Browser API, không tồn tại trên server — sẽ crash nếu dùng trong SSR (Next.js).
3. Dependency array có giá trị [a, b] — chạy lại khi dependency thay đổi
**userId** thay đổi. Nếu có nhiều giá trị trong mảng, chỉ cần một giá trị thay đổi là effect chạy lại.
Khi nào dùng: Fetch data phụ thuộc vào props hoặc state — đây là pattern phổ biến nhất trong thực tế.
4. Có cleanup, dependency [] — attach một lần, detach khi unmount
resize, scroll, keydown), timer (setInterval), kết nối một lần.
5. Có cleanup, dependency có giá trị — dọn cũ trước khi tạo mới
roomId thay đổi: cleanup unsubscribe khỏi phòng cũ trước, rồi effect subscribe vào phòng mới. Không có cleanup thì mỗi lần đổi phòng bạn vẫn nhận tin nhắn từ tất cả các phòng trước đó.
Trình tự chính xác:
6. Cleanup với blob URL — tránh memory leak
previewUrl thay đổi (người dùng chọn ảnh mới), URL cũ được giải phóng khỏi bộ nhớ trước khi URL mới được tạo.
Khi nào dùng: Bất cứ khi nào tạo resource tốn bộ nhớ cần được giải phóng thủ công — blob URL, canvas context, timer, v.v.
Lưu ý thực tế
Không bỏ dependency vào mảng để “tránh chạy lại”
Nếu effect dùng một giá trị nhưng bạn không khai báo nó trong dependency array, effect sẽ đọc giá trị cũ (stale closure) — đây là nguồn gốc của rất nhiều bug khó tìm. ESLint pluginexhaustive-deps sẽ cảnh báo bạn về điều này.
Hay gặp nhất ở 3 tình huống này:
1. Polling / interval đọc state
Đúng như ví dụ vừa rồi — dùng setInterval để gọi API định kỳ hoặc đếm ngược, nhưng quên count hoặc các filter/param trong dependency. Timer chạy nhưng luôn dùng giá trị cũ.
2. Fetch data dùng param từ state/props
page thay đổi nhưng effect không chạy lại — data vẫn là trang 1. Bug này khó nhận ra vì UI trông bình thường, chỉ data là sai.
3. Event handler bên trong effect dùng state
inputValue lúc mount. Loại bug này rất khó debug vì nhìn code có vẻ đúng hoàn toàn.
Điểm chung của cả 3 là: effect chạy một lần với [], bên trong có dùng giá trị động, nhưng không khai báo vào dependency. Cảm giác ban đầu thấy [] “an toàn” vì không lo effect chạy lại — nhưng đó chính là bẫy.
Không đặt object hay array trực tiếp vào dependency
Cleanup là bắt buộc nếu effect tạo ra “kết nối”
Event listener, subscription, interval, blob URL — tất cả đều cần cleanup. Quên cleanup không gây lỗi ngay, nhưng sẽ tích lũy thành vấn đề hiệu suất hoặc behavior kỳ lạ về sau.Tóm tắt
| Không cleanup | Có cleanup | |
|---|---|---|
Không có [] | Chạy sau mọi render | Dọn trước mỗi render tiếp theo |
[] rỗng | Chạy 1 lần lúc mount | Chạy 1 lần lúc mount, dọn khi unmount |
[a, b] | Chạy lại khi a hoặc b thay đổi | Dọn cũ → chạy lại khi a hoặc b thay đổi |
useEffect chỉ có một việc: chạy code sau khi render, và dọn dẹp khi cần. Toàn bộ sự phức tạp đến từ việc kiểm soát khi nào nó chạy qua dependency array, và có cần dọn dẹp gì không qua cleanup function.
Nắm chắc 6 pattern trên là đủ để xử lý hầu hết các tình huống thực tế.