React useEffect explained, and when you don't need it
Mosaddik Billah 4 min read

On this page
useEffect is the React hook people reach for most and misunderstand most. The short version: an effect keeps your component in sync with something outside React, such as a network request, a timer, a browser API or a third-party widget. If nothing outside React is involved, you probably don’t need one.
The basic shape#
import { useEffect } from "react";
function PageTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}React first renders the component and updates the screen, then runs the effect. Here the effect copies title into the browser tab, which lives outside React.
The dependency array#
The second argument tells React when to run the effect again:
useEffect(() => { /* ... */ }); // after every render
useEffect(() => { /* ... */ }, []); // once, after the first render
useEffect(() => { /* ... */ }, [id]); // after the first render, and whenever id changesList every value from the component that the effect uses: props, state and functions defined in the component. If you leave one out, the effect keeps using an old value. The react-hooks/exhaustive-deps lint rule catches this for you, so keep it turned on.
Cleanup#
An effect can return a function. React calls it before running the effect again, and when the component is removed:
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
return <p>{time.toLocaleTimeString()}</p>;
}Without the cleanup, every time Clock appeared on screen it would start another timer that never stopped. Anything you start in an effect, such as a timer, a subscription or an event listener, should be stopped in its cleanup.
Fetching data without race conditions#
A common bug: the user switches from product 1 to product 2, but product 1’s slower response arrives last and overwrites the screen. Cleanup fixes it:
function Product({ id }) {
const [product, setProduct] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/products/${id}`, { signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
return response.json();
})
.then(setProduct)
.catch((err) => {
if (err.name !== "AbortError") setError(err);
});
return () => controller.abort();
}, [id]);
if (error) return <p>Couldn’t load this product.</p>;
if (!product) return <p>Loading…</p>;
return <h2>{product.name}</h2>;
}When id changes, the cleanup cancels the old request before the new one starts, so only the latest response is used. In a larger app, a data library such as TanStack Query, or your framework’s data loading, handles this along with caching and retries.
Why does my effect run twice?#
In development, React’s Strict Mode mounts each component, removes it, and mounts it again, so effects run, clean up, and run again. This is deliberate: it reveals effects that are missing a cleanup. It doesn’t happen in production. If running twice breaks something, fix the cleanup rather than removing Strict Mode.
When you don’t need an effect#
Many effects only copy data from one place in React to another. They add an extra render and a chance for the two to get out of sync.
Calculating values from props or state. Calculate them during render:
// Unnecessary effect and extra state
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// Simpler
const fullName = `${firstName} ${lastName}`;If the calculation is slow, wrap it in useMemo instead of an effect.
Responding to a click or submit. Put the code in the event handler. An effect runs because something rendered; a handler runs because the user did something, which is usually what you mean:
// Do this in the handler, not in an effect that watches "submitted"
async function handleSubmit(event) {
event.preventDefault();
await saveForm(values);
showToast("Saved");
}A good test: if you can describe the code as “when the user does X”, it belongs in a handler. If it’s “while this component is on screen, stay in sync with Y”, it’s an effect.
Summary#
- Use an effect to sync with something outside React.
- List everything the effect uses in the dependency array.
- Undo in the cleanup whatever the effect starts.
- Calculate derived values during render, and put user actions in event handlers.
React is also what powers dashboard pages and extensions in Wix apps. If you need a custom Wix app built, I do Wix apps with the Wix CLI.
Comments