React’s useEffect
API is a fantastically useful hook that allows for “side effect” logic to be run in function components.
useEffect
has many uses ranging from asynchronous data fetching to interacting with browser APIs.
However!
Overusing useEffect
can cause crashes or confusing behavior in your components.
In particular I’ve developed a pet peeve around code unnecessarily wrapping synchronous variable initialization code in a useEffect
hook.
Allow me to explain why…
Bug: Initially Missing Data
Take a look at this WaitingQueue
component that renders a queue of names.
It stores a list of names in a queue with a button to pop to the next one if there are any left.
import { useEffect, useState } from "react";
function getNames() {
return ["Abed", "Annie", "Britta", "Jeff", "Pierce", "Shirley", "Troy"];
}
function WaitingQueue() {
const [remaining, setRemaining] = useState(); // 😬
useEffect(() => {
setRemaining(getNames());
}, []);
if (!remaining.length) {
return <div>Nobody left!</div>;
}
const next = () => {
setRemaining((previous) => previous.slice(1));
};
return (
<button onClick={next} role="button">
{remaining[0]} is first.
</button>
);
}
WaitingQueue
’s state management works by:
- Initially starting
remaining
off with no value:undefined
- Running a
useEffect
tosetRemaining
to the result ofgetNames()
This version of the WaitingQueue
component will crash on first render!
Because the remaining
piece of state starts off as undefined
, asking for remaining.length
throws an error.
// TypeError: cannot read property 'length' of undefined
if (!remaining.length) {
return <div>Nobody left!</div>;
}
useEffect
does not run functions immediately.
Its functions are run after render.
Creating a piece of state without an initial value and only giving it a value in a useEffect
hook means it that it will be undefined
for at least one render during the function.
Fun fact: this bug would have been caught by TypeScript! TypeScript would have seen that
remaining
is typeundefined
and complained about retrieving propertylength
on anundefined
value. [Playground link]
Bug: Initially Wrong Data
I’ve also seen a lot of gut reactions on how to fix the above crash.
One common strategy is to provide some default stub value for the state to be used until the useEffect
hook runs.
import { useEffect, useState } from "react";
function getNames() {
return ["Abed", "Annie", "Britta", "Jeff", "Pierce", "Shirley", "Troy"];
}
function WaitingQueue() {
const [remaining, setRemaining] = useState([]); // 🤔
useEffect(() => {
setRemaining(getNames());
}, []);
if (!remaining.length) {
return <div>Nobody left!</div>;
}
const next = () => {
setRemaining((previous) => previous.slice(1));
};
return (
<button onClick={next} role="button">
{remaining[0]} is first.
</button>
);
}
Since remaining
is an []
empty array in the first render pass of the component, it will include a <div>Nobody left!</div>
message instead of the message including Abed
.
We fixed the crash! 🎉
Any version of React running on the client will update the DOM extremely quickly after first render. If your app only runs on the client browser, you likely will never notice any flicker of the initial, wrong message on your screen.
Unfortunately for us, the first render of a React component can be quite important for a production website. Static site generation (SSG) and server-side rendering (SSR) are popular and important performance solutions for code. This first-render bug means the statically rendered version of the website will have the wrong first value. So this bug is still something you’ll probably want to fix.
<!-- view-source:https://localhost:3000 -->
<div>Nobody left!</div>
Solution: Initial Data
The bugfix is my favorite kind. It involves deleting code and simplifying logic.
We can remove the useEffect
altogether and start remaining
off equal to getNames()
.
import { useState } from "react";
function getNames() {
return ["Abed", "Annie", "Britta", "Jeff", "Pierce", "Shirley", "Troy"];
}
function WaitingQueue() {
const [remaining, setRemaining] = useState(getNames());
if (!remaining.length) {
return <div>Nobody left!</div>;
}
const next = () => {
setRemaining((previous) => previous.slice(1));
};
return (
<button onClick={next} role="button">
{remaining[0]} is first.
</button>
);
}
<!-- view-source:https://localhost:3000 -->
<div>Abed is first.</div>
Running a useEffect
to update the remaining
state from an initial value to an updated one was unnecessary.
Cool. Cool cool cool.
Moral of the Story
Don’t run useEffect
updates you don’t need.
If a piece of state is meant to start off pointing to data you always have access to, you can use that data as the initial value for the state.
Instead of this:
const [data, setData] = useState();
useEffect(() => {
setData(initialValue);
}, []);
…most of the time, probably do this:
const [data, setData] = useState(initialValue);
What About Other Data Behaviors?
Everything I’ve said so far about how you shouldn’t use useEffect
to initialize a synchronous value indeed only applies to synchronous values initialized once.
If the data is asynchronously loaded then sure, you’re not going to be able to start off with it available synchronously.
You might want to use a useEffect
that initializes the call to load the data:
function MyComponent({ value }) {
const [data, setData] = useState();
useEffect(() => {
loadData().then(setData).catch(setData);
}, []);
// ...
}
Even if the data is loaded synchronously and immediately available, you may still wish to add a useEffect
to reset the value.
For example, if your component is meant to reset the display whenever a prop changes, you can both pass the value as an initial state and reset it on change:
function MyComponent({ value }) {
const [data, setData] = useState(value);
useEffect(() => {
setData(value);
}, [value]);
// ...
}
Updated August 21st, 2022: @TkDodo on Twitter mentions that this could often be better solved with a
key
. ✨
Understand Your Effects
Built-in React hook APIs such as useEffect
are intentionally flexible and can be used in a variety of ways.
Understanding how they work can greatly help you avoid common pitfalls and bugs when using them.
I hope this post was useful to you in at least showing one of the ways useEffect
can blow up.
Let me know on Twitter!