Cleanup là hàm bạn trả về từ useEffect; React gọi nó trước mỗi lần chạy lại effect và một lần nữa khi component unmount. Nó không phải "hàm dọn dẹp lúc thoát" — nó chạy nhiều lần trong đời một component.
useEffect(() => {
const id = setInterval(tick, delay)
return () => clearInterval(id) // chạy trước mỗi lần effect chạy lại (delay đổi), và khi unmount
}, [delay])Nếu delay đổi, React gọi clearInterval của lần cũ rồi mới tạo interval mới. Thiếu dòng đó thì mỗi lần đổi delay để lại một interval sống mãi — đồng hồ chạy nhanh dần, và trình duyệt giữ luôn cả closure lẫn state mà nó tham chiếu.
Quy tắc thực dụng: cứ đăng ký thứ gì thì huỷ đúng thứ đó.
| Effect làm gì | Cleanup |
|---|---|
setInterval / setTimeout | clearInterval / clearTimeout |
addEventListener | removeEventListener (đúng cùng tham chiếu hàm) |
subscribe | unsubscribe |
fetch | controller.abort() |
| Mở WebSocket / kết nối | Đóng kết nối |
Chỗ hay sai với addEventListener: phải gỡ đúng tham chiếu hàm đã gắn. Truyền hai arrow function khác nhau vào hai chỗ thì removeEventListener không gỡ được gì cả, và cũng không báo lỗi.
Lưu ý: Strict Mode ở môi trường phát triển chạy mount → cleanup → mount cho mỗi lần mount, chính là để phơi ra effect thiếu cleanup ngay lúc bạn đang viết. Effect thấy "chạy hai lần" mà để lại hai kết nối là dấu hiệu cleanup còn thiếu, không phải lỗi của React.