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:
- 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
- Quản lý scope của biến tốt hơn, do bị giới hạn trong block
{}
- 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> ); }
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à callsetInterval(log, 1000)
, mỗi 1slog()
sẽ được call 1 lần, tại đây, closurelog()
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
, closurelog()
call mỗi 1s lần vẫn sử dụngcount = 0
cũ. Lúc nàylog()
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.
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”