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
remainingoff with no value:undefined -
Running a
useEffecttosetRemainingto 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
remainingis typeundefinedand complained about retrieving propertylengthon anundefinedvalue. [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!