useEffect Pet Peeve: Side Effects, Not Initialization
September 08, 2021
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 webiste. 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!