// LOADING
// LOADING
// LOADING_ARTICLE
Side effects are anything a component does beyond returning JSX: fetching data, setting up a subscription, writing to localStorage, starting a timer. React's rendering model is pure by design — given the same props and state, render the same output — and side effects are the deliberate exception to that rule. Managing them poorly is responsible for a large fraction of React bugs: stale closures, memory leaks, duplicate network requests, infinite loops.
This is not a useEffect tutorial. It is a guide to knowing when to use it, when to reach for something else, and how to structure the effects you do write.
Effects in React are synchronization mechanisms. useEffect says: "keep this external system in sync with this React state." The external system might be a WebSocket connection, a third-party library, the document title, or an animation.
If you find yourself writing a useEffect that does not synchronize with something external — that just runs some logic when state changes — there is often a better place for that logic.
tsx// Wrong: effect to compute derived state const [filteredItems, setFilteredItems] = useState(items); useEffect(() => { setFilteredItems(items.filter((i) => i.active)); }, [items]); // Right: compute during render const filteredItems = items.filter((i) => i.active);
The first version adds a render cycle: state changes, effect runs, sets new state, triggers another render. The second is instant and has no chance of inconsistency.
If something should happen because the user did something, put it in the event handler — not in an effect triggered by the state change that the event handler caused.
tsx// Wrong: effect watching a flag set by a button click const [submitted, setSubmitted] = useState(false); useEffect(() => { if (submitted) submitForm(data); }, [submitted]); // Right: call directly in the handler async function handleSubmit() { await submitForm(data); }
For a comprehensive look at dependency arrays, stale closures, and cleanup patterns in useEffect specifically, useEffect Pitfalls and How to Avoid Them covers those in depth.
Fetching data in useEffect is the canonical React pattern from the hooks era, but it comes with real costs: no request deduplication, no caching, race conditions on fast navigations, and waterfall fetches in nested components.
tsx// This works but has problems: useEffect(() => { let cancelled = false; fetch('/api/posts') .then((r) => r.json()) .then((data) => { if (!cancelled) setPosts(data); }); return () => { cancelled = true; }; }, []);
You need a cancellation flag to handle unmount races. You have no loading deduplication — if two components mount simultaneously, both fire the request. You get no automatic cache invalidation.
For client-side data fetching, a library like SWR or TanStack Query solves these problems correctly:
tsximport useSWR from 'swr'; const fetcher = (url: string) => fetch(url).then((r) => r.json()); function PostList() { const { data: posts, error, isLoading } = useSWR('/api/posts', fetcher); if (isLoading) return <p>Loading...</p>; if (error) return <p>Failed to load posts.</p>; return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>; }
In Next.js App Router, the better answer for most data is to fetch on the server in a Server Component and pass the result as props. There is no useEffect, no loading state, and no client-side waterfall:
tsx// app/notelogs/page.tsx (Server Component) export default async function NotelogsPage() { const posts = await getPosts(); // direct DB call or fetch return <PostList posts={posts} />; }
This approach aligns with React Server Components and removes entire categories of side-effect problems by moving data access out of the browser.
Here are cases where useEffect is genuinely the right tool:
tsxuseEffect(() => { const unsub = store.subscribe(() => setCount(store.getCount())); return unsub; }, []);
The cleanup function (return unsub) is critical. Without it, every re-mount creates an additional subscription that never unsubscribes — a classic memory leak.
tsxuseEffect(() => { document.title = `${unreadCount} unread messages`; }, [unreadCount]);
tsxuseEffect(() => { const chart = new Chart(canvasRef.current, config); return () => chart.destroy(); }, []);
In each of these cases, there is a clear external system (the store, the document, the chart instance) that needs to stay in sync with React state. The cleanup function reverses the effect when the component unmounts or when the dependencies change.
A few principles that prevent most effect bugs:
useEffect(() => setX(y), [])) is almost always derived state in disguise.Effects run after paint. For the common case, this is fine. But if an effect causes a visible state change that produces a flash (the content appearing, then shifting), useLayoutEffect fires synchronously before paint. Use it sparingly and only for DOM measurements or scroll position synchronization.
For effects that are expensive and run on every render, verify that the dependency array is actually minimizing re-runs. Passing object or array literals directly into a dependency array creates new references every render, causing the effect to run more often than intended. Memoize the dependency with useMemo or move the value outside the component.
These performance considerations connect directly to Understanding React Rendering and the Virtual DOM — knowing when React re-renders and why is the prerequisite to understanding when effects re-run.
Side-effect management is one part of a larger question: where does logic live in a React application? Local vs Global State in React covers the state side. Forms in React covers the mutation side. And How I Structure Large Next.js Projects covers how these pieces fit together at the project level.
If you are preparing for senior frontend interviews, side effects and the React lifecycle are among the most consistently tested topics. The interview prep section covers common questions in this space.
Side effects in React are not a feature to be maximized — they are a cost to be minimized. Derived state, event-driven logic, and server-side data fetching eliminate entire categories of effects before you write them. The effects you do write should have clear external targets, honest dependency arrays, and always a cleanup function. Treat every useEffect as something that needs justification, and you will avoid most of the class of bugs that makes React applications hard to maintain.