Dealing with side effects in React can be tricky - if you're not careful, you might end up with messy code that runs slower than it should.
Imagine how great it would feel to handle side effects like a pro, writing better code that makes the most of React's useEffect hook.
Don't worry - this isn't just talk. This friendly guide to useEffect will help you level up your React skills, taking you from "What's going on?" to "I've got this!" in no time.
Understanding side effects
As we build applications, we often need to synchronize with external systems. This can include things like:
- Making network requests
- Managing timeouts / intervals
- Reading / writing from localStorage
- Listening for global events
React call all these things “side effects”.
A side effect is what happens anytime we want to do something that’s outside of React’s typical job responsibilities, but still needs to be synchronized with the state that we’re holding in React.
<title>
tag is outside of the React scope.
But we can definitely do this in React, React gives us a very specific tool for managing these sorts of situations, and it’s called the useEffect* Hook.
The useEffect hook
You pass it a function, and whatever you do inside this function, React doesn’t really have much visibility into it, but React will call this function for us at appropriate times.
React.useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
So essentially, we’ve given React this function, and we’re telling React to call this function immediately after every render. To understand it well, you can refer to the React core loop here 👀.
The useEffect hook also takes a second argument that is always an array. That array will contain the list of dependencies (state variables) that this effect depends on.
React.useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
}, [count]);
This said to react → Only call this function when this count variable changes.
So instead of calling it automatically after a new render, now it’s going to ignore the renders where count
stays the same.
Strict Mode gotcha
When using React in your local development environment, you may notice something unexpected: effects run twice when a component first mounts.
This behavior occurs because of Strict Mode — a React setting that deliberately re-runs certain code sections during development to help identify potential problems.
Practice
Below, we have a signup form with several React state variables.
Our goal is to add a
console.log
that fires only when the value of “email” changes. We should see the user's email logged in the console whenever they edit that field.Acceptance Criteria:
→ Whenever the user changes the value of the “email” state variable, the new value should be logged to the console.
→ Nothing should be logged when the user changes another field (for example, their city or postal code).
→ The logging should be done inside an effect, not within the
onChange
event handler.Stretch Goal:
→ Update the code so that name is also logged whenever it changes.
Solution code
A single useEffect:
React.useEffect(() => {
console.log({ name, email });
}, [name, email]);
Two independent effects
React.useEffect(() => {
console.log({ email });
}, [email]);
React.useEffect(() => {
console.log({ name });
}, [name]);
Using useEffect to log state change
You might be wandering is it necessary to use useEffect in the case of logging state changes? Wouldn’t it be better to add the log to the event handler instead?
<Field
id="name"
label="Name"
value={name}
onChange={(event) => {
setName(event.target.value);
console.log(event.target.value);
}}
/>
This approach works fine here since we're only updating the name
value in one spot. But what if we needed to change this value in several different places throughout our code?
<div className="button-row">
<button
onClick={() => {
const nextCount = count + 1;
setCount(nextCount);
// We'd have to do it here...
console.log(nextCount);
}}
>
Increase
</button>
<button
onClick={() => {
const nextCount = count + 10;
setCount(nextCount);
// ...and here...
console.log(nextCount);
}}
>
Increase by 10
</button>
{/* ...and for each of the 6 buttons! */}
</div>
In cases like this, I have a clear preference for using an effect. It's a lot more “bulletproof”. I don't have to worry about forgetting to do it in 1 place; I can trust that the side-effect will always fire whenever count
changes, regardless of what the trigger is.
Now, when we only have a single trigger, as we do in this SignupForm
exercise, I honestly think either approach is fine. If you forced me to choose, I'd still do it in an effect because it guards against any future changes to the code, where name
might change somewhere else.
🔍. Similar posts
How to Effectively Use the React useId Hook in Your Apps
16 Feb 2025
The Simple Method for Lifting State Up in React Effectively
16 Feb 2025
How to Master Dynamic Key Generation in React for Your Apps
12 Feb 2025