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

推荐订阅源

博客园 - 【当耐特】
WordPress大学
WordPress大学
T
The Exploit Database - CXSecurity.com
博客园_首页
MyScale Blog
MyScale Blog
The Cloudflare Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
美团技术团队
Stack Overflow Blog
Stack Overflow Blog
博客园 - 聂微东
M
MIT News - Artificial intelligence
Microsoft Security Blog
Microsoft Security Blog
F
Full Disclosure
V
V2EX
博客园 - Franky
博客园 - 三生石上(FineUI控件)
Hugging Face - Blog
Hugging Face - Blog
P
Proofpoint News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
SecWiki News
SecWiki News
N
Netflix TechBlog - Medium
S
Secure Thoughts
酷 壳 – CoolShell
酷 壳 – CoolShell
Hacker News: Ask HN
Hacker News: Ask HN
爱范儿
爱范儿
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Webroot Blog
Webroot Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Martin Fowler
Martin Fowler
PCI Perspectives
PCI Perspectives
S
Security @ Cisco Blogs
Recorded Future
Recorded Future
Help Net Security
Help Net Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
AI
AI
Microsoft Azure Blog
Microsoft Azure Blog
K
Kaspersky official blog
G
GRAHAM CLULEY
H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
CERT Recently Published Vulnerability Notes
U
Unit 42
T
Tor Project blog
Cloudbric
Cloudbric
Hacker News - Newest:
Hacker News - Newest: "LLM"
MongoDB | Blog
MongoDB | Blog
GbyAI
GbyAI
T
The Blog of Author Tim Ferriss
Security Latest
Security Latest
N
News and Events Feed by Topic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO

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 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() 5 JavaScript Scope Gotchas
5 Mistakes to Avoid When Using React Hooks
Dmitri Pavlutin · 2020-12-08 · via Dmitri Pavlutin Blog

You've read many posts on how to use React hooks. But knowing how Not to use, sometimes, is equally important as knowing how to use.

In this post, I will describe the React hooks usage mistakes, and how to fix them.

Table of Contents

  • 1. Do Not change hooks invocation order
  • 2. Do Not use stale state
  • 3. Do Not create stale closures
  • 4. Do Not use the state for infrastructure data
  • 5. Do Not forget to cleanup side-effects
  • 6. Summary

1. Do Not change hooks invocation order

A few days before writing the post, I was coding a component that fetches game information by id. Here's a simplified version of FetchGame component:


function FetchGame({ id }) {

if (!id) {

return 'Please select a game to fetch';

}

const [game, setGame] = useState({

name: '',

description: ''

});

useEffect(() => {

const fetchGame = async () => {

const response = await fetch(`/api/game/${id}`);

const fetchedGame = await response.json();

setGame(fetchedGame);

};

fetchGame();

}, [id]);

return (

<div>

<div>Name: {game.name}</div>

<div>Description: {game.description}</div>

</div>

);

}


Open the demo.

The component FetchGame accepts a prop id — the id of the game to be fetched. useEffect() hook fetches the game information await fetch(`/game/${id}`) and saves it into the state variable game.

Open the demo and load a few games. The component correctly performs the fetch, as well as updates the state with the fetched data. But look at the Problems tab — Eslint warns about incorrect order of hooks execution:

React hooks order problem

The problem is in the early exit:


function FetchGame({ id }) {

if (!id) {

return 'Please select a game to fetch';

}

// ...

}


If id is empty, the component renders 'Please select a game to fetch' and exits. No hooks are invoked. But if id is not empty (e.g. equals '1'), then the useState() and useEffect() hooks are invoked.

The hooks executed conditionally can lead to unexpected and hard to debug errors. The way React hooks internally work requires components to invoke hooks in the same order between renderings — always!

That's exactly what suggests the first rule of hooks: Don’t call Hooks inside loops, conditions, or nested functions.

Solving the incorrect order of hooks means moving the return statement after invoking the hooks:


function FetchGame({ id }) {

const [game, setGame] = useState({

name: '',

description: ''

});

useEffect(() => {

const fetchGame = async () => {

const response = await fetch(`/api/game/${id}`);

const fetchedGame = await response.json();

setGame(fetchedGame);

};

if (id) {

fetchGame();

}

}, [id]);

if (!id) {

return 'Please select a game to fetch';

}

return (

<div>

<div>Name: {game.name}</div>

<div>Description: {game.description}</div>

</div>

);

}


Now, no matter id is empty or not, the useState() and useEffect() hooks are always invoked in the same order.

Here's a good practice that helps to avoid conditional rendering of hooks:

Execute the hooks at the top of the component body, the conditional rendering logic move to the bottom.

eslint-plugin-react-hooks can also help you enforce the correct hooks execution order.

2. Do Not use stale state

The following component MyIncreaser increases the state variable count when a button is clicked:


function MyIncreaser() {

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

const increase = useCallback(() => {

setCount(count + 1);

}, [count]);

const handleClick = () => {

increase();

increase();

increase();

};

return (

<>

<button onClick={handleClick}>Increase</button>

<div>Counter: {count}</div>

</>

);

}


The interesting part is that handleClick invokes the state update 3 times.

Now, before opening the demo, I want to ask you. If you click the button Increase once, does the counter increase by 3?

Ok. Open the demo and click Increase button once.

Unfortunately, even if the increase() is called 3 times inside the handleClick(), count increases only by 1. Hm...

The problem lays in the setCount(count + 1) state updater. When the button is clicked, React invokes setCount(count + 1) 3 times:


const handleClick = () => {

increase();

increase();

increase();

};

// same as:

const handleClick = () => {

setCount(count + 1);

// count variable is now stale

setCount(count + 1);

setCount(count + 1);

};


The first invocation of setCount(count + 1) correctly updates the counter as count + 1 = 0 + 1 = 1. However, the next 2 calls of setCount(count + 1) also set the count to 1 because they use a stale state.

The stale state is solved by using a functional way to update the state. Instead of setCount(count + 1), let's better use setCount(count => count + 1):


function MyIncreaser() {

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

const increase = useCallback(() => {

setCount(count => count + 1);

}, []);

const handleClick = () {

increase();

increase();

increase();

};

return (

<>

<button onClick={handleClick}>Increase</button>

<div>Counter: {count}</div>

</>

);

}


Open the demo.

By using an updater function count => count + 1, React gives you the latest actual state value.

Open the fixed demo. Now clicking Increase button updates the count by 3, as expected.

Here's a good rule to avoid stale state variables:

If you use current state to calculate the next state, always use a functional way to update the state: setValue(prevValue => prevValue + someResult).

3. Do Not create stale closures

React hooks heavily rely on the concept of closures. Relying on closures is what makes them so expressive.

As a quick reminder, the closure in JavaScript is the function that captures variables from its lexical scope. No matter where the closure is executed, it always has access to the variables from the place where it is defined.

When using hooks that accept callbacks as arguments (like useEffect(callback, deps), useCallback(callback, deps)) you might create a stale closure — a closure that has captured outdated state or prop variables.

Let's see a case of a stale closure created when using useEffect(callback, deps) 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);

}, []);

const handleClick = () => setCount(count => count + 1);

return (

<>

<button onClick={handleClick}>Increase</button>

<div>Counter: {count}</div>

</>

);

}


Open the demo.

Open the demo and click Increase button. Then check the console — every 2 seconds appears Count is: 0, no matter the actual value of count state variable.

Why does it happen?

At first render, the closure log captures count variable as 0.

Later, when the button is clicked and count increases, setInterval still invokes the old log closure that has captured count as 0 from the initial rendering. log is a stale closure because it has captured a stale (in other words outdated) state variable count.

The solution is to let know useEffect() that the closure log depends on count and properly reset the timer:


function WatchCount() {

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

useEffect(function() {

const id = setInterval(function log() {

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

}, 2000);

return () => clearInterval(id);

}, [count]);

const handleClick = () => setCount(count => count + 1);

return (

<>

<button onClick={handleClick}>Increase</button>

<div>Counter: {count}</div>

</>

);

}


Open the demo.

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

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

To prevent closures from capturing old values:

Make sure that any state or prop value used inside of a callback supplied to a hook is indicated as a dependency.

eslint-plugin-react-hooks can help you remember to properly set the hooks dependencies.

4. Do Not use the state for infrastructure data

Once I needed to invoke a side-effect on state update, but without invoking the side-effect on the first render. useEffect(callback, deps) always invokes the callback after mounting of the component, so I wanted to avoid that.

Surprisingly for me, I found the following solution:


function MyComponent() {

const [isFirst, setIsFirst] = useState(true);

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

useEffect(() => {

if (isFirst) {

setIsFirst(false);

return;

}

console.log('The counter increased!');

}, [count]);

return (

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

Increase

</button>

);

}


A state variable isFirst holds the information whether this is the first render of the component. Holding such information in the state is a problem — as soon as you update setIsFirst(false) another re-render happens — and for no reason.

The information on whether this is the first render shouldn't be stored in the state. The infrastructure data, like details about rendering cycle (is first rendering, the number of renderings), timer ids (setTimeout(), setInterval()), direct references to DOM elements, etc. should be stored and updated using references useRef().

Let's store the information about the first rendering into a reference isFirstRef:


function MyComponent() {

const isFirstRef = useRef(true);

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

useEffect(() => {

if (isFirstRef.current) {

isFirstRef.current = false;

return;

}

console.log('The counter increased!');

}, [count]);

return (

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

Increase

</button>

);

}


isFirstRef is a reference that holds the information whether this is the first rendering of the component. isFirstRef.current property is used to access and update the value of the reference.

What's important: updating a reference isFirstRef.current = false doesn't trigger re-rendering.

5. Do Not forget to cleanup side-effects

A lot of side-effects, like making a fetch request or using timers like setTimeout(), are asynchronous.

Do not forget to clean up the side-effect if the component unmounts or it doesn't need anymore the result of the side-effect.

For example, if you've started a timer, make sure to stop the timer when the component unmounts.

The following component has a button Start increasing. When the button is clicked, a counter increases by 1 each second:


function DelayedIncreaser() {

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

const [increase, setShouldIncrease] = useState(false);

useEffect(() => {

if (increase) {

setInterval(() => {

setCount(count => count + 1)

}, 1000);

}

}, [increase]);

return (

<>

<button onClick={() => setShouldIncrease(true)}>

Start increasing

</button>

<div>Count: {count}</div>

</>

);

}


Open the demo, and click Start increasing button. As expected, the count state variable increases each second.

While having the increase in progress, click the Unmount Increaser button to umount the component. React warns in the console about updating the state of an unmounted component.

React unmounted component update state warning

Fixing DelayedIncreaser is simple: just use a cleanup function from the callback of useEffect() to stop the interval timer:


function DelayedIncreaser() {

// ...

useEffect(() => {

if (increase) {

const id = setInterval(() => {

setCount(count => count + 1)

}, 1000);

return () => clearInterval(id);

}

}, [increase]);

// ...

}


Open the fixed demo. Click Start Increasing button and check how the count increases. Then hit Unmount Increaser: and thanks to () => clearInterval(id) cleanup the interval stops. No complaints from React.

That being said, every time you code a side-effect, question yourself whether it should clean up. Timers, heavy fetch requests (like uploading files), sockets — almost always have to be clean up.

6. Summary

The best way to start with React hooks is to learn how to use them.

But you can encounter situations when you can't understand why they behave differently than you expect. Knowing how to use React hooks is not enough: you also should know how Not to use them.

The first thing Not to do is render the hooks conditionally or change the order of hooks invocation. React expects that, no matter the props or state values, the component always invokes the hooks in the same order.

To avoid stale state values use a functional way to update the state.

Don't forget to indicate the dependencies for hooks that accept callbacks as arguments: e.g. useEffect(callback, deps), useCallback(callback, deps). That allows you to solve the stale closures.

Do not store infrastructure data (like information about component rendering cycle, setTimeout() or setInterval() ids) into the state. The rule of thumb is to keep such data in references.

Do not forget to clean up your side-effects, if that's the case.

One more nasty pitfall you can encounter when using useEffect() is the infinite loop. Check out my post on How to Solve the Infinite Loop of React.useEffect() .

What other React hooks anti-patterns do you know?