惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
Webroot Blog
Webroot Blog
AI
AI
P
Palo Alto Networks Blog
Attack and Defense Labs
Attack and Defense Labs
WordPress大学
WordPress大学
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
Spread Privacy
Spread Privacy
T
Tor Project blog
罗磊的独立博客
小众软件
小众软件
S
Security Affairs
酷 壳 – CoolShell
酷 壳 – CoolShell
量子位
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
NISL@THU
NISL@THU
博客园_首页
PCI Perspectives
PCI Perspectives
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
腾讯CDC
Security Latest
Security Latest
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Cloudflare Blog
A
About on SuperTechFans
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
TaoSecurity Blog
TaoSecurity Blog
宝玉的分享
宝玉的分享
G
GRAHAM CLULEY
雷峰网
雷峰网
F
Full Disclosure
I
Intezer
Cloudbric
Cloudbric
博客园 - 三生石上(FineUI控件)
U
Unit 42

Dmitri Pavlutin Blog

Pure Functions in JavaScript: A Beginner's Guide Record Type in TypeScript: A Quick Intro How to Write Comments in React: The Good, the Bad and the Ugly 4 Ways to Create an Enum in JavaScript React forwardRef(): How to Pass Refs to Child Components TypeScript Function Types: A Beginner's Guide How to Use v-model to Access Input Values in Vue Mastering Vue refs: From Zero to Hero Environment Variables in JavaScript: process.env 5 Must-Know Differences Between ref() and reactive() in Vue How to Destructure Props in Vue (Composition API) Triangulation in Test-Driven Development How to Use nextTick() in Vue Programming to Interface Vs to Implementation A Smarter JavaScript Mapper: array.flatMap() Array Grouping in JavaScript: Object.groupBy() How to Access ES Module Metadata using import.meta JSON Modules in JavaScript How to Trim Strings in JavaScript TypeScript Function Overloading How to Debounce and Throttle Callbacks in Vue How to Show/Hide Elements in Vue Sparse vs Dense Arrays in JavaScript How to Fill an Array with Initial Values in JavaScript Covariance and Contravariance in TypeScript What are Higher-Order Functions in JavaScript? How to Use TypeScript with React Components Index Signatures in TypeScript How to Use React useReducer() Hook unknown vs any in TypeScript A Guide to React Context and useContext() Hook How to Use Promise.any() 2 Ways to Remove a Property from an Object in JavaScript 'return await promise' vs 'return promise' in JavaScript How to Use Promise.allSettled() How to Use fetch() with JSON JavaScript Promises: then(f,f) vs then(f).catch(f) What is a Promise in JavaScript? How to Use Promise.all() A Simple Guide to Component Props in React Don't Stop Me Now: How to Use React useTransition() hook A Simple Explanation of JavaScript Variables: const, let, var ES Modules Dynamic Import How to Memoize with React.useMemo() How to Cleanup Async Effects in React Why Math.max() Without Arguments Returns -Infinity How to Debounce and Throttle Callbacks in React Don't Confuse Function Expressions and Function Declarations in JavaScript How to Use ES Modules in Node.js Solving a Mystery Behavior of parseInt() in JavaScript How to Use Array Reduce Method in JavaScript 3 Ways to Merge Arrays in JavaScript A Guide to Jotai: the Minimalist React State Management Library The Difference Between Values and References in JavaScript How to Implement a Queue in JavaScript A Helpful Algorithm to Determine "this" value in JavaScript React useRef() Hook Explained in 3 Steps 7 Interview Questions on "this" keyword in JavaScript. Can You Answer Them? How to Greatly Enhance fetch() with the Decorator Pattern 7 Interview Questions on JavaScript Closures. Can You Answer Them? What's a Method in JavaScript? array.sort() Does Not Simply Sort Numbers in JavaScript How to Solve the Infinite Loop of React.useEffect() The New Array Method You'll Enjoy: array.at(index) What's the Difference between DOM Node and Element? Why Promises Are Faster Than setTimeout()? Everything About Callback Functions in JavaScript How React Updates State 5 Mistakes to Avoid When Using React Hooks 5 Best Practices to Write Quality JavaScript Variables Type checking in JavaScript: typeof and instanceof operators 3 Ways to Check if a Variable is Defined in JavaScript React Forms Tutorial: Access Input Values, Validate, Submit Forms Prototypal Inheritance in JavaScript How to Timeout a fetch() Request How to Learn JavaScript If You're a Beginner A Simple Explanation of React.useEffect() A Simple Explanation of JavaScript Iterators How to Use React Controlled Inputs Everything about null in JavaScript How to Use Fetch with async/await Getting Started with Arrow Functions in JavaScript An Interesting Explanation of async/await in JavaScript Front-end Architecture: Stable and Volatile Dependencies Is it Safe to Compare JavaScript Strings? How to Access Object's Keys, Values, and Entries in JavaScript What Actually is a String in JavaScript? 3 Ways to Shallow Clone Objects in JavaScript (w/ bonuses) Checking if an Array Contains a Value in JavaScript JavaScript Event Delegation: A Beginner's Guide How to Parse URL in JavaScript: hostname, pathname, query, hash 3 Ways to Detect an Array in JavaScript How to Get the Screen, Window, and Web Page Sizes in JavaScript 3 Ways to Check If an Object Has a Property/Key in JavaScript How to Compare Objects in JavaScript Object.is() vs Strict Equality Operator in JavaScript Own and Inherited Properties in JavaScript 5 Differences Between Arrow and Regular Functions How to Use Object Destructuring in JavaScript Your Guide to React.useCallback()
Be Aware of Stale Closures when Using React Hooks
Dmitri Pavlutin · 2019-10-24 · via Dmitri Pavlutin Blog

Hooks ease the management of state and side effects inside functional React components. Moreover, repeated logic can be extracted into a custom hook to reuse across the application.

Hooks heavily rely on JavaScript closures. That's why hooks are so expressive and simple. But closures are sometimes tricky.

One issue you can encounter when using hooks is stale closure. And it might be difficult to identify a stale closure!

Let's start with distilling what stale closure is. Then you'll see how a stale closure affects React hooks, and how to solve that.

1. The stale closure

A factory function createIncrement(incBy) returns a tuple of increment and log functions. When called, increment() function increases the internal value by incBy, while log() simply logs a message with the information about the current value:


function createIncrement(incBy) {

let value = 0;

function increment() {

value += incBy;

console.log(value);

}

const message = `Current value is ${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 "Current value is 0"


Try the demo.

[increment, log] = createIncrement(1) returns a tuple of functions: one function that increments the internal value, and another that logs the current value.

Then the 3 invocations of increment() increment value up to 3.

Finally, the call of log() logs the message "Current value is 0". Hmm... this is unexpected because value equals 3.

log() is a stale closure. The closure log() has captured message variable having "Current value is 0".

Even if value variable gets incremented multiple times when calling increment(), the message variable doesn't update and always keeps an outdated value "Current value is 0".

The stale closure captures variables that have outdated values.

Let's see some approaches on how to fix the stale closure.

2. Fixing the stale closure

Fixing the stale log() requires closing the closure over the changed variable: value.

Let's move the statement const message = ...; into log() function body:


function createIncrement(incBy) {

let value = 0;

function increment() {

value += incBy;

console.log(value);

}

function log() {

const message = `Current value is ${value}`; // correct!

console.log(message);

}

return [increment, log];

}

const [increment, log] = createIncrement(1);

increment(); // logs 1

increment(); // logs 2

increment(); // logs 3

// Works!

log(); // logs "Current value is 3"


Try the fixed demo.

Now, after calling 3 times the increment() function, calling log() logs the actual value: "Current value is 3".

log() is no longer a stale closure. message accesses the actual value variable every time log() is called.

3. State closure in useEffect()

Let's study a common case of stale closure when using useEffect() hook.

Inside the component <WatchCount> the hook useEffect() logs every 2 seconds the value of count:


function WatchCount() {

const [count, setCount] = useState(0);

useEffect(function() {

setInterval(function log() {

console.log(`Count is: ${count}`);

}, 2000);

}, []);

return (

<div>

{count}

<button onClick={() => setCount(count + 1) }>

Increase

</button>

</div>

);

}


Open the demo and click a few times increase button. Then look at the console: every 2 seconds appears Count is: 0, although count state variable has been increased a few times.

React Stale Closure

Why does it happen?

At first render, the state variable count is initialized with 0.

After the component has mounted, useEffect() calls setInterval(log, 2000) timer function which schedules calling log() function every 2 seconds. Here, the closure log() captures count variable as 0.

Later, even if count increases when the Increase button is clicked, the log() closure called by the timer function every 2 seconds still uses count as 0 from the initial render. log() becomes a stale closure.

The solution is to let know useEffect() that the closure log() depends on count and properly handle the reset of the interval when count changes:


function WatchCount() {

const [count, setCount] = useState(0);

useEffect(function() {

const id = setInterval(function log() {

console.log(`Count is: ${count}`);

}, 2000);

return function() {

clearInterval(id);

}

}, [count]);

return (

<div>

{count}

<button onClick={() => setCount(count + 1) }>

Increase

</button>

</div>

);

}


With the dependencies properly set, useEffect() updates the closure as soon as count changes.

Open the fixed demo and click a few times increase. The console will log the actual value of count.

React Stale Closure Fixed

Proper management of hooks dependencies is an efficient way to solve the stale closure problem.

I recommend enabling eslint-plugin-react-hooks, which detects forgotten dependencies.

On a side note, sometimes you can use a ref instead of a state in a React component. A ref is a mutable object whose value you access using ref.current, thus it's a benefit because you cannot get a stale closure on a ref. ref.current always evaluates to the actual value of the ref.

4. State closure in useState()

The component <DelayedCount> has 1 button Increase async that increments the counter asynchronously with 1 second delay.


function DelayedCount() {

const [count, setCount] = useState(0);

function handleClickAsync() {

setTimeout(function delay() {

setCount(count + 1);

}, 1000);

}

return (

<div>

{count}

<button onClick={handleClickAsync}>Increase async</button>

</div>

);

}


Now open the demo. Click quickly 2 times Increase async button. The counter gets updated only by 1, instead of the expected 2.

On each click setTimeout(delay, 1000) schedules the execution of delay() after 1 second. delay() captures the variable count as being 0.

Both delay() closures (because 2 clicks have been made) update the state to the same value: setCount(count + 1) = setCount(0 + 1) = setCount(1).

All because the delay() closure of the second click has captured the outdated count variable as being 0.

To fix the problem, let's use a functional way setCount(count => count + 1) to update count state:


function DelayedCount() {

const [count, setCount] = useState(0);

function handleClickAsync() {

setTimeout(function delay() {

setCount(count => count + 1);

}, 1000);

}

function handleClickSync() {

setCount(count + 1);

}

return (

<div>

{count}

<button onClick={handleClickAsync}>Increase async</button>

<button onClick={handleClickSync}>Increase sync</button>

</div>

);

}


Now setCount(count => count + 1) updates the count state inside delay().

Open the demo. Click Increase async quickly 2 times. The counter displays the correct value 2.

When a callback that returns the new state based on the previous one is supplied to the state update function, React makes sure that the latest state value is supplied as an argument to that callback:


setCount(alwaysActualStateValue => newStateValue);


That's why the stale closure problem that appears during state update is usually solved by using a functional way to update the state.

5. Conclusion

The stale closure problem occurs when a closure captures outdated variables.

An efficient way to solve stale closures is to correctly set the dependencies of React hooks. Or, in the case of a stale state, use a functional way to update the state.

The key takeaway is to try to supply hooks with closures that capture the freshest variables.

The next step to mastering React hooks is to be aware of 5 Mistakes to Avoid When Using React Hooks.