Bạn đã bao giờ gặp trường hợp: Bạn gọi setCount(count + 1) trong một setInterval nhưng số count mãi mãi chỉ tăng từ 0 lên 1 rồi dừng lại?
Chào mừng bạn đến với Stale Closure (Đóng vùng nhớ bị cũ) - một trong những khái niệm khó nhằn nhất trong React.
1. Bản chất: Closure là một "Bức ảnh" (Snapshot)
Trong JavaScript, một function được định nghĩa bên trong một function khác sẽ "ghi nhớ" (capture) tất cả các biến ở phạm vi bên ngoài tại thời điểm nó được tạo ra.
Hãy xem ví dụ kinh điển này:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
// 📸 Tại thời điểm mount, count = 0.
// Hàm setInterval này "chụp" và giữ chặt giá trị count = 0 đó mãi mãi.
console.log("Count hiện tại:", count);
setCount(count + 1); // Sẽ luôn thực hiện 0 + 1
}, 1000);
return () => clearInterval(id);
}, []); // [] có nghĩa là effect này không bao giờ chạy lại
}Mặc dù setCount đã chạy, nhưng lần gọi tiếp theo của setInterval vẫn sử dụng cái "bức ảnh" lúc count = 0.
2. Tại sao Hooks lại dễ bị bẫy?
Bởi vì bản chất của React Functional Component là chạy lại toàn bộ function ở mỗi lần render.
Mỗi lần render là một "thế giới" hoàn toàn mới với các biến const hoàn toàn mới. Nếu một hàm (như callback của useEffect) được tạo ra ở "thế giới render #1", nó sẽ mãi mãi mang theo những giá trị của "thế giới #1", ngay cả khi ứng dụng đã render đến "thế giới #100".
3. Cách "phá bẫy" chuẩn Senior
Giải pháp 1: Functional Updates (Khuyên dùng)
Đây là cách sạch sẽ nhất. Bạn không sử dụng biến count từ closure, mà yêu cầu React đưa cho bạn giá trị mới nhất thực tế đang nằm trong bộ nhớ (internal state).
setCount(prevCount => prevCount + 1); // prevCount luôn là giá trị thực tế nhấtGiải pháp 2: Dependency Array (Trung thực với React)
Hãy đưa count vào danh sách phụ thuộc. React sẽ hủy effect cũ và chạy lại effect mới với "bức ảnh" count mới nhất.
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, [count]); // Chạy lại mỗi khi count đổi -> Closure luôn mớiLưu ý: Cách này có thể gây lãng phí hiệu năng nếu effect làm việc nặng (như mở kết nối Socket).
Giải pháp 3: useEvent (Pattern tương lai)
Trong tương lai (hoặc dùng Custom Hook tương đương), chúng ta có thể dùng useRef để giữ một tham chiếu đến hàm callback luôn mới nhất mà không làm trigger re-render.
const latestCount = useRef(count);
latestCount.current = count; // Luôn cập nhật ref
useEffect(() => {
setInterval(() => {
console.log(latestCount.current); // Luôn lấy được giá trị mới nhất!
}, 1000);
}, []); // Không cần phụ thuộc vào countKết luận
- Stale Closure không phải là lỗi của React, đó là cách JavaScript vận hành.
- Quy tắc: Nếu bạn dùng một biến bên trong một hook async (timer, API, event listener), hãy luôn tự hỏi: "Biến này có bị cũ không?".
- Hãy tin tưởng tuyệt đối vào Linter của React. Nếu nó báo thiếu dependency, đừng lờ nó đi bằng
// eslint-disable-line.
Thấu hiểu Closure là dấu mốc quan trọng để bạn bước từ một người "biết dùng React" sang một "kỹ sư React" thực thụ.