0
0
Lập trình
Harry Tran
Harry Tran106580903228332612117

Cách Khắc Phục Vấn Đề 'Stale Closure' Trong React

Đăng vào 2 tháng trước

• 5 phút đọc

Giới thiệu

Bạn đã bao giờ gặp tình huống kỳ lạ trong React khi gọi setState, nhưng giá trị bên trong hàm xử lý sự kiện vẫn là giá trị cũ? Hoặc khi sử dụng setInterval, bạn nhận thấy rằng state luôn trả về giá trị ban đầu? Nếu có, bạn không đơn độc. Một trong những nguyên nhân chính dẫn đến hiện tượng này là vấn đề stale closure. Trong bài viết này, chúng ta sẽ khám phá cách thức hoạt động của closures, lý do vì sao stale closure xảy ra trong React, các ví dụ điển hình, và cách khắc phục chúng.

Mục lục

  1. Tổng Quan Về Scope và Closures
  2. Tại Sao Stale Closures Xảy Ra Trong React
  3. Các Tình Huống Thường Gặp Khi Gặp Vấn Đề Này
  4. Cách Khắc Phục Vấn Đề Stale Closure
  5. Cách useRef Giúp Tránh Stale Closures
  6. Kết Luận

1. Tổng Quan Về Scope và Closures

Scope là gì?

Scope là “phạm vi mà một biến tồn tại.” Ví dụ, các biến được tạo ra bên trong một hàm không thể được truy cập từ bên ngoài:

javascript Copy
function foo() {
  const x = 10;
  console.log(x); // 10
}
foo();

console.log(x); // ❌ Lỗi: x không tồn tại ở đây

Closure là gì?

Một closure là “cơ chế mà một hàm ghi nhớ các biến từ môi trường mà nó được tạo ra.” Ví dụ:

javascript Copy
function outer() {
  const message = "Hello";

  function inner() {
    console.log(message);
  }

  return inner;
}

const fn = outer();
fn(); // "Hello"

Thông thường, khi outer kết thúc, message sẽ bị xóa. Nhưng vì inner ghi nhớ phạm vi khi nó được tạo ra, nó vẫn có thể truy cập message. Hãy nghĩ về một hàm như một hộp thời gian mang theo một tập hợp các biến từ thời điểm nó được tạo ra.

2. Tại Sao Stale Closures Xảy Ra Trong React

Các thành phần React là hàm, vì vậy một phạm vi mới được tạo ra cho mỗi lần render. Ví dụ:

javascript Copy
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log("count:", count); // ← giữ nguyên giá trị 0 mãi mãi
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

Khi nhấn nút, count được cập nhật, nhưng bên trong setInterval, count vẫn giữ giá trị ban đầu là 0. Điều này xảy ra vì closure được tạo trong useEffect([]) luôn giữ phạm vi ban đầu mãi mãi. Nói cách khác, bạn đang mắc kẹt với một stale closure - một closure bị mắc kẹt với phạm vi cũ.

3. Các Tình Huống Thường Gặp Khi Gặp Vấn Đề Này

  • Callbacks cho setInterval / setTimeout
  • Vòng lặp với requestAnimationFrame
  • Các hàm xử lý sự kiện từ WebSocket hoặc addEventListener
  • Các callbacks bất đồng bộ (then, async/await) đọc state

Chủ đề chung: một hàm được đăng ký một lần sẽ tiếp tục sống lâu dài.

4. Cách Khắc Phục Vấn Đề Stale Closure

① Chỉ Định Các Dependencies Đúng Cách

Cách khắc phục đơn giản nhất là bao gồm state trong mảng dependency của useEffect:

javascript Copy
useEffect(() => {
  const id = setInterval(() => {
    console.log("count:", count); // luôn là giá trị mới nhất
  }, 1000);
  return () => clearInterval(id);
}, [count]);

Nhưng hãy cẩn thận: hiệu ứng sẽ đăng ký lại mỗi khi có sự thay đổi, điều này có thể ảnh hưởng đến hiệu suất hoặc quản lý tài nguyên.

② Sử Dụng Functional setState

Để cập nhật state, bạn có thể sử dụng dạng hàm của setState, luôn nhận giá trị mới nhất bất kể closures:

javascript Copy
setCount(prev => prev + 1);

Cách này tránh stale closures và là mẫu an toàn nhất.

③ Sử Dụng useRef (Giải Pháp Mạnh Mẽ Để Tránh Stale Closure)

Đây là nơi useRef phát huy tác dụng.

5. Cách useRef Giúp Tránh Stale Closures

useRef Là Gì?

useRef tạo ra một hộp lưu trữ tồn tại qua các lần render:

javascript Copy
const ref = useRef(0);

ref.current = 123;
console.log(ref.current); // 123
  • Lưu trữ giá trị trong ref.current
  • Cập nhật nó không không kích hoạt render lại
  • Hữu ích không chỉ cho DOM refs, mà còn cho việc duy trì các biến

Ví Dụ: Khắc Phục Stale Closure

javascript Copy
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  // Phản ánh giá trị count mới nhất vào ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  useEffect(() => {
    const id = setInterval(() => {
      console.log("Giá trị count mới nhất:", countRef.current); // luôn cập nhật
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}
  • Bên trong setInterval, đọc countRef.current
  • Không còn stale closure nữa - luôn là giá trị mới nhất.

Nâng Cao: Lưu Trữ Các Hàm Trong useRef

Bạn cũng có thể lưu các hàm bên trong ref để luôn gọi logic mới nhất:

javascript Copy
const callbackRef = useRef<(val: number) => void>(() => {});

useEffect(() => {
  callbackRef.current = (val: number) => {
    console.log("Giá trị count mới nhất:", count, "val:", val);
  };
}, [count]);

// Ví dụ: gọi từ các sự kiện bên ngoài
socket.on("message", (val) => {
  callbackRef.current(val);
});

6. Kết Luận

  • Closures ghi nhớ phạm vi từ khi hàm được tạo ra.
  • Stale closures là những closures bị mắc kẹt với phạm vi cũ.
  • Trong React, chúng thường xuất hiện trong các interval, hàm xử lý sự kiện, callbacks bất đồng bộ, v.v.
  • Các giải pháp:
    1. Chỉ định dependencies đúng cách.
    2. Sử dụng functional setState.
    3. Sử dụng useRef để duy trì giá trị hoặc hàm mới nhất.

👉 Một stale closure giống như một “cái bug du hành thời gian trong React.” Một hàm tiếp tục mang theo một phạm vi cũ vào tương lai - và đó là lý do tại sao state của bạn “không cập nhật.”

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào