How to Use useEffect in React

What you’ll build or solve

You’ll run side effects in a React component at the right time, like syncing with browser APIs, timers, or event listeners.

When this approach works best

useEffect works best when you need to:

  • Sync with something outside React, like document.title, localStorage, or a third-party widget.
  • Set up and clean up timers, event listeners, or subscriptions.
  • React to a specific prop or state change with a side effect, like updating the URL or focusing an input.

Skip useEffect when you can compute the value during render. If the result depends only on props and state, derive it directly instead of storing it via an effect.

Prerequisites

  • A React app set up (Vite, CRA, Next.js, etc.)
  • Basic React knowledge (components, props, state)

Step-by-step instructions

1) Add an effect that runs after render

useEffect runs after React finishes rendering and updates the DOM. Put your side-effect code in the callback.

JavaScript

import {useEffect,useState }from"react";

exportdefaultfunctionProfile() {
const [name,setName]=useState("Sam");

useEffect(() => {
document.title=`Profile:${name}`;
  });

return (
<div>
<p>{name}</p>
<buttononClick={() =>setName("Alex")}>Change name</button>
</div>
  );
}

Without a dependency array, the effect runs after every render. That works for cheap sync work, but it can be too much for effects that set up ongoing work.


2) Control when effects run with the dependency array

The dependency array (the second argument) changes when the effect runs.

Run once on mount

Use [] to run the effect when the component mounts.

JavaScript

import {useEffect }from"react";

exportdefaultfunctionWelcome() {
useEffect(() => {
console.log("Mounted");
  }, []);

return<p>Welcome</p>;
}

Run when values change

List the values your effect uses. React reruns the effect when any dependency changes.

JavaScript

import {useEffect,useState }from"react";

exportdefaultfunctionCounterTitle() {
const [count,setCount]=useState(0);

useEffect(() => {
document.title=`Count:${count}`;
  }, [count]);

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

Run after every render (rare)

Omit the array when you truly want the effect after every render.

JavaScript

useEffect(() => {
console.log("Rendered");
});

Use this sparingly. If you see repeated work you did not expect, a missing dependency array is often the reason.

What to look for:

  • Include every prop and state value you read inside the effect in the dependency array.
  • If a dependency changes every render (like an inline object or function), the effect will also rerun every render.

3) Clean up side effects with a return function

If your effect sets up something that keeps running, return a cleanup function. React runs cleanup before rerunning the effect and when the component unmounts.

JavaScript

import {useEffect,useState }from"react";

exportdefaultfunctionWindowWidth() {
const [width,setWidth]=useState(() =>window.innerWidth);

useEffect(() => {
functiononResize() {
setWidth(window.innerWidth);
    }

window.addEventListener("resize",onResize);

return () => {
window.removeEventListener("resize",onResize);
    };
  }, []);

return<p>Width: {width}px</p>;
}

What to look for:

  • Cleanup runs before the next effect run, not just on unmount.
  • Timers, listeners, subscriptions, and in-flight async work should not keep running after unmount.

Examples you can copy

Example 1: Sync document.title with state

JavaScript

import {useEffect,useState }from"react";

exportdefaultfunctionTitleSync() {
const [name,setName]=useState("Sam");

useEffect(() => {
document.title=`Hello,${name}`;
  }, [name]);

return (
<label>
      Name:{" "}
<input
value={name}
onChange={(e) =>setName(e.target.value)}
/>
</label>
  );
}

Example 2: Start and stop a timer

import {useEffect,useState }from"react";

exportdefaultfunctionStopwatch() {
const [seconds,setSeconds]=useState(0);
const [running,setRunning]=useState(false);

useEffect(() => {
if (!running)return;

constid=setInterval(() => {
setSeconds((s) =>s+1);
    },1000);

return () =>clearInterval(id);
  }, [running]);

return (
<div>
<p>{seconds}s</p>
<buttononClick={() =>setRunning(true)}>Start</button>
<buttononClick={() =>setRunning(false)}>Stop</button>
<buttononClick={() =>setSeconds(0)}>Reset</button>
</div>
  );
}

Example 3: Subscribe to a browser event

JavaScript

import {useEffect,useState }from"react";

exportdefaultfunctionOnlineStatus() {
const [online,setOnline]=useState(() =>navigator.onLine);

useEffect(() => {
functiononOnline() {
setOnline(true);
    }

functiononOffline() {
setOnline(false);
    }

window.addEventListener("online",onOnline);
window.addEventListener("offline",onOffline);

return () => {
window.removeEventListener("online",onOnline);
window.removeEventListener("offline",onOffline);
    };
  }, []);

return<p>{online?"Online":"Offline"}</p>;
}

Common mistakes and how to fix them

Mistake 1: Creating an infinite loop by updating state every render

What you might do:

useEffect(() => {
setCount(count+1);
});

Why it breaks:

The effect runs after render, updates state, which triggers another render, and repeats.

Fix: Add a dependency array and use a functional update when you only need “increment once” behavior.

useEffect(() => {
setCount((c) =>c+1);
}, []);

Mistake 2: Missing dependencies (stale values inside the effect)

What you might do:

useEffect(() => {
console.log("Query:",query);
}, []);

Why it breaks:

The effect captures the initial query, so later updates never reach it.

Fix: Include what you use.

useEffect(() => {
console.log("Query:",query);
}, [query]);

Troubleshooting

  • If you see an effect firing twice in development, check React Strict Mode. React 18 can run effects twice in dev to surface unsafe side effects. Test a production build to confirm real behavior.
  • If you see “Maximum update depth exceeded,” look for an effect that updates state without a dependency array, or dependencies that change every render.
  • If you see “Can’t perform a React state update on an unmounted component,” add cleanup. Timers, listeners, subscriptions, and async work should stop on unmount.
  • If an effect reruns “for no reason,” check for unstable dependencies like inline objects and functions. Move them inside the effect, or memoize them with useMemo or useCallback.

Quick recap

  • Put side effects inside useEffect(() => { ... }).
  • Use [] for mount-only effects, and [deps] to rerun when values change.
  • Return a cleanup function to stop timers, listeners, and subscriptions.
  • Include every value you read inside the effect in the dependency array.
  • Watch for infinite loops and Strict Mode double-runs in development