Stale closure là khi một hàm vẫn nhìn thấy giá trị của lần render đã cũ. Nó không phải lỗi của React mà là hệ quả bình thường của closure trong JavaScript: mỗi lần render tạo ra một bộ biến mới, và hàm nào được tạo ở lần render nào thì giữ lại (capture) giá trị của lần đó.
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
console.log(count) // luôn in 0, dù count đã tăng
setCount(count + 1) // luôn đặt thành 1
}, 1000)
return () => clearInterval(id)
}, []) // deps rỗng → effect chỉ chạy một lần
...
}Effect chạy đúng một lần với count bằng 0, nên hàm bên trong setInterval giữ mãi số 0 đó. Màn hình đứng ở 1 mãi mãi và không có lỗi nào được ném ra.
Ba cách chữa, chọn theo tình huống:
// 1. Khai báo đúng dependency — effect chạy lại khi count đổi
useEffect(() => { ... }, [count])
// 2. Dùng updater — không cần đọc count nữa nên deps rỗng là đúng
setCount((c) => c + 1)
// 3. Dùng ref khi cần đọc giá trị mới nhất mà không muốn effect chạy lại
const countRef = useRef(count)
countRef.current = countCách 2 là tốt nhất khi giá trị mới chỉ phụ thuộc giá trị cũ. Cách 3 hợp với listener hay timer sống lâu, nhưng dùng nhiều là dấu hiệu effect đang ôm quá nhiều việc.
Lưu ý: Quy tắc react-hooks/exhaustive-deps bắt gần hết loại lỗi này ngay lúc viết. Mỗi lần bạn định thêm // eslint-disable-next-line cho nó, khả năng cao là bạn đang tạo ra một stale closure.