Skip to content
wannacode

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.

React

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.

React

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
  1. Render: React calls StockWatcher and gets the markup. The function only computes what to show. It starts nothing.
  2. The component is on the page. Now React runs the effect: the timer starts and counts the checks.
  3. The buyer picks another product. The prop changes, StockWatcher renders again, and React compares [product] with the last time: it is different.
  4. 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.
  5. The effect runs again with the new product. Its own timer counts from #1.
  6. The buyer closes the banner: the component leaves the page. The last cleanup runs, and the timer stops with it.
  7. 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

The buyer changes the product, but the checks are all for the first one. Which line is to blame?

Pick the line with the bug.

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]);

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.

React

Test

Check yourself

1. A component starts setInterval and never stops it. The buyer opens and closes the banner three times. How many timers are running?
2. The effect reads product, and its dependency list is []. The buyer changes the product. What happens?
3. When does the cleanup of an effect run?
4. Which of these is NOT a job for an effect?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·