Latest Post

Tăng thứ hạng và truy cập tự nhiên với 10 phương pháp SEO hay nhất Kiếm Tiền Online (mmo): Khái Niệm và Các Hình Thức Phổ Biến

1. Closure là gì ?

Closures là một kỹ thuật lập trình không đặc trưng cho JavaScript mà hầu hết các ngôn ngữ lập trình khác cũng có thể sử dụng. Theo định nghĩa trên MDN:

Closure là một khái niệm trong lập trình, đề cập đến việc các hàm có thể tham chiếu đến các biến tự do (free variable) một cách độc lập. Nói một cách khác, khi bạn định nghĩa một hàm trong một closure, nó sẽ lưu giữ và ghi nhớ môi trường (environment) trong đó nó được tạo ra.

Đọc qua thì sẽ hơi khó hiểu nhưng để mình giải thích đơn giản lại như thế này:

Closure là những function chứa những function con và các biến, bên trong function con có thể sử dụng những biến do function cha nhận vào hoặc khai báo (ghi nhớ lại môi trường được khởi tạo)

Mình sẽ có 1 ví dụ closure trong javascript để cho các bạn dễ hiểu

function greeting(greetingWord){
    // Chú ý vào scope của biến _greetingWord dấu {} chứa nó
    // Thông thường sử dụng _ làm tiền tố của những biến private
    let _greetingWord = greetingWord
    return function sayGreeting(name){
        // _greetingWord được lấy từ function bên ngoài do
        console.log(`${_greetingWord} ${name}`)
    }
}
const speakHello = greeting('Hello')
speakHello('Vương') //logs: Hello Vương
const speakHi = greeting('Hi')
speakHi('Quân') //logs: Hi Quân

Ở đây chúng ta thấy biến _greetingWord được tạo từ function cha greeting và trong các function con (function đang được return ra ngoài) sẽ truy cập vào biến này để sử dụng khi cần thiết

Chú ý vào phạm vi của biến (scope) _greetingWord sẽ được sử dụng trong toàn bộ function cha bao gồm cả function con dựa theo block của { }.

greeting được gọi là closure

Lợi ích:

  1. Khi hiểu rõ về scope của biến và closure, sử dụng thành thạo closure sẽ giúp sử dụng lại code tốt hơn
  2. Quản lý scope của biến tốt hơn, do bị giới hạn trong block {}
  3. Tạo ra nhiều biến thể của function (function con)

2. Stale Closure là gì?

function createIncrement(incBy) {
  let value = 0;
  function increment() {
    value += incBy;
    console.log(value);
  }
  const message = `Giá trị hiện tại là ${value}`;
  function log() {
    console.log(message);
  }
  return [increment, log];
}
const [increment, log] = createIncrement(1);
increment(); // logs: 1
increment(); // logs: 2
increment(); // logs: 3
// Does not work!
log(); // logs: "Giá trị hiện tại là 0"

Ở đây khi gọi hàm log(), mong muốn của mình sẽ log ra giá trị là 3, nhưng giá trị lại là 0. Đúng bằng value = 0 ban đầu.

Lỗi xẩy ra do message được tạo ra khi gọi hàm createIncrement và không được cập nhật mỗi lần increment

Stale closure là một biến trong closure bị outdate

3. Hướng dẫn xử lý

Cách 1: Di chuyển code khởi tạo message vào trong hàm log, mỗi khi gọi hàm log thì message sẽ được tạo lại và đảm bảo không bị outdate

Cách 2: Nếu message được sử dụng ở nhiều function, chúng ta sẽ khai bao biến message ở ngoài function cha và thay đổi message ở trong increment

// Cách 1
function createIncrement(incBy) {
  let value = 0;
  function increment() {
    value += incBy;
    console.log(value);
  }
  function log() {
    const message = `Giá trị hiện tại là ${value}`;
    console.log(message);
  }
  return [increment, log];
}
const [increment, log] = createIncrement(1);
increment(); // logs 1
increment(); // logs 2
increment(); // logs 3
// Works!
log(); // logs "Giá trị hiện tại là 3"
// Cách 2
function createIncrement(incBy) {
  let value = 0;
  let message = `Giá trị hiện tại là ${value}`;
  function increment() {
    value += incBy;
    console.log(value);
    message = `Giá trị hiện tại là ${value}`;
  }
  function log() {
    console.log(message);
  }
  return [increment, log];
}

4. Stale closure trong React

4.1 useEffect()

Chúng ta cùng nhau tìm hiểu một trường hợp rất hay gặp stale closure trong React là hook useEffect

Bên trong component Count, hook useEffect sẽ logs ra biến count mỗi 2s một lần:

function Count() {
    const [count, setCount] = useState(0);
    useEffect(() => {
        setInterval(function log() {
            console.log(`Count is: ${count}`);
        }, 1000);
    }, []);
    return (
        <div>
            {count}
            <button onClick={() => setCount(count + 1)}>
                Increase
            </button>
        </div>
    );
}
Hình ảnh thực tế

Khi chúng ta click increment để thay đổi state, nhưng count trong useEffect() vẫn mang giá trị là 0. Tại sao lại như thế? mình sẽ giải thích ở phía dưới đây:

  • Ở lần render đầu tiên, count sẽ được khởi tạo với giá là 0
  • Sau khi component render ra UI lần đầu, useEffect sẽ được thực thi và call setInterval(log, 1000), mỗi 1s log() sẽ được call 1 lần, tại đây, closure log() ghi nhớ biến count có giá trị là 0
  • Ở những lần sau đó, mặc dù biến count đã thay đổi giá trị khi mình click increase, closure log() call mỗi 1s lần vẫn sử dụng count = 0 cũ. Lúc này log() trở thành stale closure.

Cách giải quyết tình trạng này là để useEffect() được thực thi lại mỗi khi count thay đổi, closure log() phụ thuộc vào count sẽ được tạo mới mỗi khi count thay đổi.

Hình ảnh thực tế

Như vậy là việc quản lý dependencyList trong useEffect() đã giải quyết được vấn đề trên

Trong thực tế đôi lúc chúng ta sẽ quên để giá trị vào dependencyList, mình gợi ý các bạn sử dụng eslint-plugin-react-hooks để kiểm tra xem bạn có quên đưa biến vào dependencies hay không

4.2 useState()

Component Count có 1 button “Increase async“ dùng để tăng giá trị count sau 1s

function Count() {
    const [count, setCount] = useState(0);
    function delayClickAsync() {
        setTimeout(function delay() {
            console.log('delay', count)
            setCount(count + 1);
        }, 1000);
    }
    return (
        <div>
            {count}
            <button onClick={delayClickAsync}>Increase async</button>
        </div>
    );
}

Mỗi khi chúng ta click, setTimeout(delay, 1000) sẽ lên lịch thực hiện delay() vào 1s sau.

Lỗi xẩy ra khi chúng ta click liên tục trong khoảng thời gian bé hơn 1s. Do sau khi click lần đầu, component chưa re-render và update lại count nhưng lại tiếp tục click liên tục, sẽ có vô số delay() được lên lịch thực hiện, nhưng thay vì lấy giá trị count mới, delay() lại lấy giá trị của lần render trước đó. Vì thế giá trị count ở những lần click sau do chưa kịp update nên vẫn mang giá trị của lần render trước, stale closure đã xẩy ra ở đây.

Để fix lỗi này, chúng ta sẽ cần setState() bằng cách sử dụng functional. setCount((count) => count + 1) để update count:

function Count() {
    const [count, setCount] = useState(0);
    function delayClickAsync() {
        setTimeout(function delay() {
            setCount(count => count + 1);
        }, 1000);
    }
    return (
        <div>
            {count}
            <button onClick={delayClickAsync}>Increase async</button>
        </div>
    );
}

Bây giờ, setCount(count => count + 1) giá trị count sẽ được cập nhật mỗi lần setCount

Bên trong setCount(), React sẽ kiểm tra giá trị truyền vào, nếu là function thì React sẽ call nó và truyền vào giá trị state hiện tại và lỗi stale closure sẽ không còn nữa.

5. Kết luận

Stale closure xảy ra khi chúng ta sử dụng closure và giá trị của biến trở nên lỗi thời (outdated). Cách đơn giản và hiệu quả nhất để khắc phục vấn đề này là làm cho closure cập nhật lại giá trị của biến bằng cách đặt biến đó vào phần dependencies khi sử dụng React hook.

Điểm quan trọng là cần đảm bảo rằng biến được cập nhật khi giá trị của nó thay đổi. Hy vọng rằng thông qua giải thích trên, bạn đã hiểu một phần về stale closure và cách xử lý khi gặp phải tình trạng tương tự. Ví dụ đã được trình bày chỉ là một tình huống đơn giản mà chúng ta có thể gặp phải. Trong thực tế, các logic của chúng ta thường phức tạp và khó nhận biết stale closure, chỉ khi gặp phải lỗi và tích lũy kinh nghiệm mới có thể giúp bạn phản xạ nhanh hơn khi gặp vấn đề tương tự.

One thought on “Lỗi “Stale Closure” trong React

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *