Effects
In short: An effect is code for side actions (a timer, a request, the tab title) that useEffect runs after render and cleans up before the next run and when the component goes away.
Problem
The banner is gone, the checks are not
The cheese banitsa is out of stock. The buyer presses "Watch the stock" and a banner appears below the button. While the banner is open, the page checks the stock every second. Here the check only writes a line to the console.
Console
Press "Watch the stock", wait two seconds and press "Stop watching". The banner disappears, yet "Check #3", "Check #4"… keep coming in the console. Open and close the banner once more: now there are two lines a second. The timer belongs to the browser, not to the component. Nobody stopped it.
The idea
Rendering only computes
A component says what should be on the screen: from props and state it computes the markup. That is rendering. The library repeats it whenever it wants to and compares the result with the previous one.
That is why rendering has to be pure: it does nothing except compute the markup. A timer, a request to the server, a change of document.title, a subscription to events on window do something outside. They are side effects.
The body of the StockWatcher function is the rendering. It runs on every render, and in the example above it starts a new timer each time. It does not show here, because the banner renders once. But if the timer changes state (say, a count of checks on the screen), every change renders again, and every render starts one more timer. Their number grows every second.
A side effect needs two things that rendering does not give: a moment to start and a moment to stop.
The idea
Effect and cleanup
An effect is code that the component runs once it is on the page. The cleanup is the code that stops what was started when the component goes away.
import { useEffect } from 'react';
export default function StockWatcher({ product }) { useEffect(() => { const id = setInterval(check, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Watching the stock of {product.name}.</p>;}useEffect(function, dependencies)tells React: "run this function once the changes are in the DOM".- The function that the effect returns is the cleanup. React calls it when the component leaves the page.
- The second argument is the list of dependencies: the values the effect reads. The next idea is about it.
The cleanup mirrors the effect: setInterval and clearInterval, addEventListener and removeEventListener, a request and cancelling that request.
Try it
Stop the checks
The timer in StockWatcher starts right in the component, and nobody stops it. Move it into an effect that starts when the banner appears, and stop it when the banner goes away. The check opens the banner, waits for "Check #1", closes it and makes sure "Check #2" never comes.
Console
Solution
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCT = { id: 4, name: 'Cheese banitsa' };
export default function App() {
const [watching, setWatching] = useState(false);
return (
<main>
<h1>{PRODUCT.name}</h1>
<p>Out of stock. It will be back soon.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Stop watching' : 'Watch the stock'}
</button>
{watching && <StockWatcher product={PRODUCT} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Check #${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Watching the stock of {product.name}. We will tell you as soon as it is back.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
The timer goes into useEffect. The effect returns () => clearInterval(id), and the dependencies are [product]. The check catches an effect without return: the timer still stays. It also catches return clearInterval(id); without the arrow: then clearInterval is called right away, the timer stops before it checks even once, and React gets undefined instead of a function.
The idea
Dependencies
Now the buyer can pick another out-of-stock product while the banner is open. The effect checks product. As soon as the product changes, the old check has to stop and a new one has to start for the new product.
That is what the dependency list is for. After every render React compares each value in it with the last time. If at least one is different, it first calls the cleanup of the old run, then runs the effect again.
| The second argument | When the effect runs |
|---|---|
[product] |
after the first render and after every render where product is different |
[] |
only after the first render |
| none | after every render |
The rule: the list holds every value from props and state that the effect reads. If product is missing, the effect stays with the old product. The effect function was created during the first render and remembers the values from then.
See it
See the life of an effect
The banner opens for the banitsa, the buyer picks the cucumbers, then closes the banner.
function StockWatcher({ product }) { useEffect(() => { let checks = 0; const id = setInterval(() => { checks++; console.log(`Check #${checks}: ${product.name}`); }, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Watching the stock of {product.name}</p>;}Step by step, in words
- Render: React calls StockWatcher and gets the markup. The function only computes what to show. It starts nothing.
- The component is on the page. Now React runs the effect: the timer starts and counts the checks.
- The buyer picks another product. The prop changes, StockWatcher renders again, and React compares [product] with the last time: it is different.
- Before the new run, the cleanup of the old one: its timer stops. Without it the old checks would go on next to the new ones.
- The effect runs again with the new product. Its own timer counts from #1.
- The buyer closes the banner: the component leaves the page. The last cleanup runs, and the timer stops with it.
- Every run has a pair: whatever the effect starts, its cleanup stops. Before the next run and when the component leaves.
The effect runs twice and the cleanup runs twice: once before the new run, once when the component is removed. In between there is always exactly one timer.
Try it
Find the bug
Pick the line with the bug.
[] means "only after the first render". The effect does not run again for the new product, and its function remembers the old one. It should be [product].Try it
Fill in
What goes in the blank so that the timer stops when the component goes away?
useEffect(() => {
const id = setInterval(check, 1000);
() => clearInterval(id);
}, [product]);return: the function the effect returns is the cleanup. React calls it before the next run and when the component is removed.The idea
When you do not need an effect
An effect is for a link to something outside the component: a timer, a server, the tab title, window. Two things look like an effect, but are not.
- A value computed from others. The cart total is not kept in a separate piece of state that an effect updates. It is computed during render (or with
useMemo), as in the lesson on derived state. An effect that writes it into separate state is extra work and a second place that can drift away from the cart. - A response to an action. The order is sent because the buyer pressed "Order". That goes in the click handler, not in an effect that watches some flag.
What is left is the real job of an effect: to keep the component in sync with the outside world while the component is on the page.
More precisely, if you are curious
The double run in development. In a new Vite project main.jsx wraps <App /> in <StrictMode>. In development StrictMode deliberately runs every effect, cleans it up and runs it again, right when it is shown. If the console says "the effect started" twice, that is a test, not a bug: an effect with a correct cleanup survives it. The sandbox does not use StrictMode, so here the effect starts once.
Why not useEffect(async () => …). An async function returns a promise, and React expects a cleanup function or nothing. The console says useEffect must not return anything besides a function, which is used for clean-up. The async function is written inside the effect and called there. That is in the next lesson.
How the dependencies are compared. With Object.is, as with state. An object created anew on every render is different every time, and the effect runs every time. The rule "everything the effect reads is in the list" is guarded by the linter (react-hooks/exhaustive-deps), which is part of the Vite template.
Class components have the same in three methods: componentDidMount, componentDidUpdate and componentWillUnmount.
Exercise
Another product
Above the banner there is a "Product" drop-down with the banitsa and the cucumbers. Right now StockWatcher starts the timer once, when it is shown. Make it so that when the product changes, the old check stops and a new one starts from #1 for the new product. The check opens the banner, waits for "Check #1" for the banitsa, picks the cucumbers, waits for "Check #1" for them, makes sure "Check #2" for the banitsa never comes, and closes the banner.
Console
Solution
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCTS = [
{ id: 4, name: 'Cheese banitsa' },
{ id: 13, name: 'Cucumbers' },
];
export default function App() {
const [productId, setProductId] = useState(4);
const [watching, setWatching] = useState(false);
const product = PRODUCTS.find((p) => p.id === productId);
return (
<main>
<label>
Product{' '}
<select value={productId} onChange={(e) => setProductId(Number(e.target.value))}>
{PRODUCTS.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</label>
<h1>{product.name}</h1>
<p>Out of stock. It will be back soon.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Stop watching' : 'Watch the stock'}
</button>
{watching && <StockWatcher product={product} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Check #${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Watching the stock of {product.name}. We will tell you as soon as it is back.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
The dependency list becomes [product], and the effect returns () => clearInterval(id). On a change React first calls the cleanup of the old timer, then runs the effect with the new product. The check catches [product] without a cleanup: then both timers run side by side.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Props: data from above · Conditions: empty, error, loading · State · Derived state
- Continues in: Data from an API in a component · Speed: when a component renders again · Testing components · From the sandbox to a project
- Tasks on this topic: Effects, data and forms