useEffect — Handling the Work Outside Rendering
A look at React's useEffect, starting from the concept of a 'side effect.' How it differs from rendering, when each of the three dependency-array forms runs, and the common misunderstandings.
At the end of last time I left this question: when state changes, what if you want to do something other than draw the screen? Like sending a request to the server every time the search term changes. The hook for that “work outside the screen” is useEffect.
If useState is the side that “changes state to update the screen,” useEffect is the opposite side — one that “performs some action in step with state or rendering.”
What is a side effect?
In Part 1 we said a component is “a function that takes state and returns a screen.” That return — rendering — must be pure. The same input yields the same screen, and it doesn’t touch the outside world along the way; that’s what makes it predictable.
But real apps are constantly entangled with the outside world. They fetch data from a server, start timers, subscribe to browser events, write logs. Work that happens outside rendering’s pure calculation like this is called a side effect. useEffect is the tool that lifts such work out of the render flow and runs it safely after the screen has been drawn.
import { useEffect } from 'react';
Basic syntax
useEffect(() => {
// perform the side effect here
}, [dependencies]);
There are two arguments:
- first: the function to run (the setup function)
- second: the dependency array — the list of values that decides when this effect re-runs
The key is this: an effect runs not during rendering but right after the screen is drawn. And not every time — it re-runs only when a value in the dependency array has changed.
The dependency array is everything
useEffect’s behavior comes down to how you set that second argument. There are three cases.
// 1) [value] — once after the first render + whenever that value changes
useEffect(() => {
console.log('count changed:', count);
}, [count]);
// 2) [] — exactly once, after the first render (for setup)
useEffect(() => {
console.log('component first appeared');
}, []);
// 3) array omitted — every single render
useEffect(() => {
console.log('something rendered');
});
| Second argument | When it runs | Main use |
|---|---|---|
[a, b] | after first render + when a or b changes | reacting to specific value changes |
[] | once after first render | initial setup (subscriptions, first load) |
| (none) | every render | rarely used |
One common point of confusion. Whether or not there’s a dependency array, an effect runs at least once, right after the first render. Writing [count] doesn’t mean it runs “only when count changes” — it’s “once at first + whenever count changes after.” If you don’t need that initial run, you have to filter it out with a condition inside the effect.
A common misconception: don’t use it everywhere
useEffect is powerful, so the newer you are, the easier it is to overuse. But these two are classic cases where you don’t need useEffect:
- A value computed from other state: for example, if you build
fullNamefromfirstNameandlastName, you don’t need an effect to make another piece of state — just compute it during render. - Responding to a user event: reacting to a button click is the job of an event handler like
onClick, not an effect.
Remember useEffect as the tool you reach for when you need “synchronization with the outside world that rendering alone can’t do,” and you’ll avoid overusing it.
Where there’s a setup, there’s cleanup
If you start a timer or connect to a server in useEffect, you need to turn those off when the component goes away. Turning things on without ever turning them off leads to problems like memory leaks or duplicate connections.
For this, useEffect lets the setup function return another function, so you can attach a ‘cleanup’ step.
useEffect(() => {
const id = setInterval(tick, 1000); // setup: start the timer
return () => clearInterval(id); // cleanup: stop the timer
}, []);
Exactly when and why this cleanup function runs is quite confusing at first. So next time we’ll take useEffect’s cleanup function on its own and cover it in detail.