Himanshu SharmaSoftware engineer
Let’s talk

React · Lesson 22 of 38

Effects with useEffect

Learn what React effects are for, how useEffect runs after rendering, how the dependency array works and why StrictMode runs effects twice.

18 minEffects and dataUpdated 24 October 2026

By the end of this lessonWrite an effect to synchronise a component with something outside React, and explain when it runs.

Rendering must be pure: a component calculates JSX from its props and state and does nothing else. But apps have to do things beyond drawing: set the page title, start a timer, listen to the window size, connect to a server, fetch data. React calls these side effects. Some belong in event handlers because a user action causes them. Others have no user action behind them: they must happen because the component is on screen. For those, React gives you the useEffect hook.

Your first effect

An effect is a function you pass to useEffect. React calls it after it has put the component on the page:

jsx
import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  console.log("1. render with count", count);

  useEffect(() => {
    console.log("2. effect runs after render, count is", count);
  });

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

export default App;
result
Count: 0
CONSOLE
1. render with count 0
1. render with count 0
2. effect runs after render, count is 0
2. effect runs after render, count is 0

The log shows the order: first the render (the function body), then the effect. (Everything is doubled because StrictMode runs the first render, and its effect, twice in development, as explained below.) Because the effect runs after the render has been committed, it never delays what the user sees. With no second argument, this effect runs after every render.

The dependency array

Running after every render is rarely what you want. The second argument, the dependency array, tells React when the effect needs to run again:

Call When it runs
useEffect(fn) after every render
useEffect(fn, []) once, after the first render
useEffect(fn, [a, b]) after the first render, and again after any render where a or b changed
jsx
import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [other, setOther] = useState(0);

  useEffect(() => {
    console.log("effect: count changed to", count);
  }, [count]);

  return (
    <div>
      <button className="count" onClick={() => setCount(count + 1)}>count {count}</button>
      <button className="other" onClick={() => setOther(other + 1)}>other {other}</button>
    </div>
  );
}

export default App;
result
count 1other 1
CONSOLE
effect: count changed to 0
effect: count changed to 0
effect: count changed to 1

Clicking "count" runs the effect again; clicking "other" re-renders the component but, because count did not change, the effect is skipped. React compares each dependency with its previous value using Object.is. The rule for the array is simple: list every reactive value (props, state, variables derived from them) that your effect uses. The next lesson covers this in depth, and the linter helps you.

Effects synchronise with the outside world

The classic examples are all about connecting React to something it does not control:

jsx
import { useEffect, useState } from "react";

function App() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);   // cleanup
  }, []);

  useEffect(() => {
    document.title = "Width: " + width;
  }, [width]);

  return <p>The preview is {width}px wide. Resize the window and watch this number.</p>;
}

export default App;

Two effects, two jobs: one subscribes to the browser's resize event, and one keeps document.title in step with the width. The browser window, its events and the title are things outside React, which is exactly what effects are for. Typical uses:

  • setting document.title,
  • adding and removing event listeners on window or document,
  • starting and stopping timers and intervals,
  • connecting to a chat server or WebSocket,
  • fetching data when a screen appears,
  • controlling a non-React widget (a map, a chart).

Keep separate concerns in separate effects instead of one big effect, as above. Each effect should describe one way of synchronising.

Cleanup: undoing what the effect did

If the effect starts something, it should usually also stop it. Return a function from the effect: React calls it before running the effect again, and when the component is removed.

jsx
import { useEffect } from "react";

function App() {
  useEffect(() => {
    console.log("connect");
    return () => console.log("disconnect");        // cleanup
  }, []);

  return <p>A component that connects while it is on screen.</p>;
}

export default App;
result
A component that connects while it is on screen.
CONSOLE
connect
disconnect
connect

You might expect just "connect". The Console shows connect, disconnect, connect. This is StrictMode, and it is on purpose.

Why StrictMode runs the effect twice

In development, React mounts the component, immediately runs the cleanup (as if the user had navigated away), and mounts it again. It exists to test that your effect can start, stop and start again without leaving a mess (duplicate connections, leaked listeners). If your cleanup is correct, the extra cycle is invisible; if you forgot it, you will see two listeners or two connections and notice the bug at once. This only happens in development, in the strict wrapper; the production build runs the effect once. Never "fix" the double run by removing StrictMode or by adding a flag. Fix the missing cleanup.

Effects versus event handlers

The most useful question to ask is: what causes this code to run?

Cause Where the code goes
The user did something (click, type, submit) event handler
The component appeared on screen, or a value it depends on changed effect

Sending an order to the server when the user presses "Buy" is an event handler. Fetching the product details because the product page appeared is an effect. Likewise, "when this prop changes, also change this state" is usually not a good use of an effect; calculate the value during rendering instead (see You might not need an effect).

jsx
import { useEffect, useState } from "react";

function App() {
  const [liked, setLiked] = useState(false);

  // Wrong place for this: the click is the cause, not the like state.
  // Right place: inside handleClick, below.
  function handleClick() {
    setLiked(true);
    console.log("handler: sending like to server");
  }

  useEffect(() => {
    console.log("effect: liked is", liked);
  }, [liked]);

  return <button onClick={handleClick}>{liked ? "Liked" : "Like"}</button>;
}

export default App;
result
Liked
CONSOLE
effect: liked is false
effect: liked is false
handler: sending like to server
effect: liked is true

Both lines print, but only the handler's message is tied to the click. If the "send to server" code lived in the effect, it would also run on the first render (when liked is false), and again whenever the component re-mounted, even though the user did nothing.

What an effect cannot do

  • Effects run on the client only. They do not run during server rendering, so they are safe places to touch window and document.
  • An effect function cannot be async directly, because it must return either nothing or a cleanup function, and an async function returns a promise. Define an async function inside it and call it (the fetching lesson shows this).
  • The values an effect uses are from the render that created it (the snapshot idea again). A timer that reads count sees the count from its own render unless you list it as a dependency or use an updater function.

A complete example: a ticking clock

jsx
import { useEffect, useState } from "react";

function App() {
  const [time, setTime] = useState(() => new Date());

  useEffect(() => {
    const id = setInterval(() => setTime(new Date()), 1000);
    return () => clearInterval(id);          // stop the timer when the component goes away
  }, []);

  return <p>The time is {time.toLocaleTimeString()}</p>;
}

export default App;

The effect starts a timer when the component appears and stops it when it leaves. The empty dependency array is correct because the effect uses no reactive values (setTime is stable and does not need to be listed). Without the cleanup, a timer would keep running after the component disappeared, calling setTime on something that no longer exists and leaking memory. With StrictMode's extra cycle, a missing clearInterval would show up immediately as a clock that ticks twice as fast.

Common mistakes

  • Forgetting the dependency array, so the effect runs after every render (and may cause an infinite loop if it sets state).
  • Leaving out dependencies, so the effect uses stale values.
  • Starting a timer, listener or connection without returning a cleanup.
  • Putting event-driven code (sending a form) in an effect.
  • Making the effect function async.
  • Fixing StrictMode's double effect by hiding it instead of writing a cleanup.
  • Using an effect to calculate something that could be computed during rendering.

Exercise

  1. Write an effect that sets document.title to the number of clicks on a button.
  2. Add a keydown listener on window that logs the pressed key, and remove it in the cleanup. Check in the Console that StrictMode does not leave two listeners.
  3. Create a countdown from 10 that stops at 0, with an interval that is cleared both when it reaches 0 and when the component unmounts.
  4. Explain, in one sentence each, why "send a like to the server" is an event handler and "subscribe to window resize" is an effect.

Key points

  • An effect is code that runs after the component has rendered, to synchronise it with a system outside React: the page title, a timer, a subscription, a network request.
  • useEffect(setup, dependencies) runs setup after every render where a dependency changed; an empty array runs it once after the first render, and no array runs it after every render.
  • Effects are not for responding to clicks: do that in event handlers. Use an effect when the component being on screen is the reason for the code.
  • In development, StrictMode runs each effect, its cleanup and the effect again once, to check that your effect can start and stop safely.

Check yourself

1. When does the function you pass to useEffect run?
2. What does useEffect(fn, []) do?
3. Where should you send a form to the server after the user clicks Submit?
4. Why do you see an effect run twice in development?