Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
useEffect & Side Effects
Side effects are operations that reach outside a component: API calls, timers, DOM manipulation, logging, etc. TheuseEffect hook is how React handles these operations in functional components.
Real-world analogy: Think of a React component as a chef who only cooks (renders JSX). Side effects are everything that is not cooking — ordering ingredients (API calls), setting a kitchen timer (setTimeout), tuning the radio (event listeners). The chef cannot do these things while plating a dish, so they happen after the dish is served. That is exactly when useEffect runs — after the component renders and the DOM updates. And just like a responsible chef cleans up after cooking, the cleanup function in useEffect tears down timers, unsubscribes from listeners, and cancels pending requests.
What is a Side Effect?
Basic useEffect Syntax
The Dependency Array
The dependency array controls when your effect runs:1. No Dependency Array — Runs After Every Render
2. Empty Array — Runs Once on Mount
3. With Dependencies — Runs When Dependencies Change
Visual Guide
Cleanup Functions
Some effects need cleanup: unsubscribing, clearing timers, removing listeners. If you start something, you need to stop it — otherwise you get memory leaks and ghost subscriptions that keep running after the component is gone.When Cleanup Runs
Event Listener Example
Data Fetching
One of the most common use cases foruseEffect.
Basic Fetch
Handling Race Conditions
When fetching data based on changing props, you can have race conditions. Imagine a user types “re”, then “rea”, then “reac” quickly. Three fetches fire. If the “re” response arrives after the “reac” response (because the server was slower for that query), you’d show stale results for “re” instead of “reac”. This is a race condition.Using AbortController
The proper way to cancel fetch requests:Common Patterns
1. Sync with External System
2. Update Document Title
3. Local Storage Sync
4. Debounced API Call
5. Intersection Observer (Infinite Scroll)
useEffect Pitfalls
1. Infinite Loops
2. Stale Closures
3. Async Functions
When NOT to Use useEffect
This section is arguably the most important part of this chapter. Over-usinguseEffect is the single biggest source of bugs and unnecessary complexity in React codebases. The React team themselves have said: “You might not need an effect.”
Some things don’t need useEffect:
Transforming Data
Handling Events
React Strict Mode and Double Effects
Why your effect runs twice in development: In React 18+ with Strict Mode enabled (the default in Create React App and Vite templates), React intentionally unmounts and remounts every component on initial mount during development. This means your
useEffect fires, then the cleanup runs, then the effect fires again.This is not a bug. React does this to help you find effects that are missing cleanup. If your effect breaks when it runs twice (e.g., it adds two event listeners, creates two subscriptions, or appends duplicate DOM elements), that is a sign your cleanup function is missing or incomplete.In production, effects only run once. If the double-fire bothers you during development, fix the cleanup function rather than disabling Strict Mode.🎯 Practice Exercises
Exercise 1: Fetch with Loading States
Exercise 1: Fetch with Loading States
Exercise 2: Real-time Clock
Exercise 2: Real-time Clock
Exercise 3: Online/Offline Status
Exercise 3: Online/Offline Status
Summary
| Concept | Description |
|---|---|
| Side Effect | Operation outside React’s render flow |
| useEffect | Hook for managing side effects |
| No dependencies | Runs after every render (rarely wanted) |
Empty [] | Runs once on mount |
[dep1, dep2] | Runs when dependencies change |
| Cleanup function | Returned function for cleanup |
| Race conditions | Handle with flags or AbortController |
| Stale closures | Use functional updates or add dependencies |
| Derived data | Calculate during render, not in effects |
Next Steps
In the next chapter, you’ll learn about the Context API — managing global state without prop drilling!
Interview Deep-Dive
Explain the useEffect cleanup function lifecycle. When exactly does cleanup run, and what happens if you forget it?
Explain the useEffect cleanup function lifecycle. When exactly does cleanup run, and what happens if you forget it?
Strong Answer:
The cleanup function runs in two scenarios: before the effect re-runs (when dependencies change) and when the component unmounts. The exact sequence when a dependency changes is: React renders new output, commits DOM changes, runs the previous effect’s cleanup, then runs the new effect. This ordering matters — cleanup runs with the old props/state values because it is the closure from the previous render.Forgetting cleanup causes three categories of production bugs. First, memory leaks: subscriptions, intervals, and event listeners keep accumulating. I once debugged a dashboard where every navigation to a page added a new WebSocket connection that was never closed. After 50 navigations, there were 50 open sockets, each receiving and processing messages, causing the tab to consume gigabytes of memory and lag severely.Second, state updates on unmounted components: an async fetch completes after the user navigates away, calling
setState on a component that no longer exists. React 17 and earlier showed a warning; React 18 silently ignores it, but the unnecessary work still happens.Third, stale event handlers: if you add a window resize listener without cleanup, each re-render adds another listener. After 100 renders, a single resize event calls your handler 100 times. I have seen this cause layout thrashing so severe it dropped the app to 2fps.The mental model I use: if you start something (timer, listener, subscription, connection), you must stop it. If you allocate something (abort controller, observer), you must clean it up. The cleanup function is the “undo” button for whatever the effect did.Follow-up: In React 18 Strict Mode, effects run twice on mount in development. Why does React do this, and what does it expose?React 18 Strict Mode intentionally mounts, unmounts, and remounts components in development to surface missing cleanup bugs. The sequence is: mount (effect runs), unmount (cleanup runs), mount again (effect runs again). If your effect has no cleanup, the second mount creates a duplicate subscription/timer/listener.This behavior does not happen in production. It exists solely as a development-time check for effects that would break in concurrent rendering scenarios, where React might discard and re-execute a render. If your effect does not survive a mount-unmount-remount cycle cleanly, it has a bug that concurrent features (like Suspense) could trigger in production.The practical test: if you see doubled API calls, doubled subscriptions, or doubled timers in development with Strict Mode, your cleanup function is missing or incomplete. Fix the cleanup, and the double-mount becomes harmless because the first instance is properly cleaned up before the second runs.When should you NOT use useEffect? Walk me through the common anti-patterns and what to do instead.
When should you NOT use useEffect? Walk me through the common anti-patterns and what to do instead.
Strong Answer:
The React team themselves say “you might not need an effect,” and I think overuse of useEffect is the single biggest source of unnecessary complexity in React codebases. Here are the patterns I actively watch for in code reviews:Transforming data for display: if you have items in state and need a filtered version, do not put filtered items in a separate state and sync them with useEffect. Just compute it during render:
const filtered = items.filter(i => i.active). If the computation is expensive, wrap it in useMemo. Using useEffect creates an extra render cycle and a synchronization bug waiting to happen.Resetting state when a prop changes: do not use useEffect(() => { setLocalState(prop) }, [prop]). This causes a flash of stale state on the first render, then the effect corrects it on the second. Instead, use a key prop to remount the component, or compute the derived value directly. If you genuinely need to reset local state when a prop changes, the key approach is cleanest.Responding to user actions: if a button click should trigger an API call, do it in the onClick handler, not in an effect triggered by a submitted flag. Effects are for synchronizing with external systems, not for running event-driven logic. The event handler is the right place because it has direct access to the context (which button, which form data, whether the user confirmed).Notifying parent components: do not use useEffect(() => { onValueChange(value) }, [value]) to notify a parent of state changes. This creates cascading renders and is hard to trace. Instead, call the callback at the point where the value actually changes (in the event handler or state setter).The rule of thumb: useEffect is for synchronizing React with something external — a DOM API, a third-party library, a WebSocket, a timer. If both the input and the output are within React’s world, you probably do not need an effect.Follow-up: A developer argues that fetching data is not a “side effect” because it is essential to the component’s purpose. Should data fetching use useEffect?Data fetching IS a side effect — it reaches outside the React component to talk to a server. Whether it is “essential” does not change its nature. The question is whether useEffect is the best tool for data fetching, and increasingly the answer is “not directly.”The problems with raw useEffect for data fetching: no built-in caching (refetching on every mount), no deduplication (multiple components fetching the same data), manual loading/error states, race condition handling, no prefetching, and no integration with Suspense.Modern approaches: React Query / TanStack Query gives you caching, background refetching, deduplication, retry logic, and pagination out of the box. Next.js and Remix move fetching to the server via Server Components or loaders, eliminating the client-side effect entirely. React’s use() hook (experimental) integrates with Suspense for data fetching.My recommendation: use useEffect for data fetching in simple apps or learning exercises. For production apps, use React Query or a framework’s built-in data fetching. The useEffect is still there under the hood in these libraries, but they handle all the edge cases you would otherwise implement manually.How do you handle race conditions in data fetching with useEffect? Compare the boolean flag approach versus AbortController.
How do you handle race conditions in data fetching with useEffect? Compare the boolean flag approach versus AbortController.
Strong Answer:
Both solve the same problem — preventing a stale response from overwriting fresh data — but at different levels.The boolean flag approach sets a local variable
let isCancelled = false inside the effect. The cleanup function sets isCancelled = true. After the fetch resolves, the handler checks if (!isCancelled) before calling setState. This is simple and works, but the HTTP request still completes on the network. The server does all the work, the response travels back, the browser parses it — you just ignore the result. For expensive server operations or metered APIs, this is wasteful.AbortController actually cancels the request at the network level. You create a controller, pass signal: controller.signal to fetch, and call controller.abort() in the cleanup. The browser cancels the TCP connection, the server may stop processing (if it checks for disconnection), and no response is transferred. This saves bandwidth, reduces server load, and is faster because the browser does not buffer the discarded response.In practice, I always use AbortController when available because the cost is the same as the boolean flag (one extra object creation) but the benefit is real resource savings. The only additional handling needed is catching the AbortError and ignoring it, since aborting causes fetch to reject: catch (err) { if (err.name !== 'AbortError') setError(err.message) }.Where AbortController does not help: XMLHttpRequest-based libraries (older Axios versions), GraphQL subscriptions, or WebSocket messages. For those, the boolean flag is the fallback.Follow-up: You have a search input that fires a fetch on every keystroke. Besides race conditions, what other problems arise and how do you solve them?Three problems beyond race conditions. First, excessive requests: a user typing “react hooks” sends 11 requests (one per character). Use debouncing — delay the fetch by 300ms after the last keystroke. If the user types another character within 300ms, cancel the previous timer. The net effect: one request for “react hooks” instead of 11.Second, rate limiting: your API may throttle at 10 requests per second. Even with debouncing, fast typers or multiple users can hit limits. Add client-side throttling as a safeguard, and handle 429 responses gracefully with exponential backoff.Third, result ordering: even without race conditions, results should match the current query. If the user types “re”, then “react”, debouncing ensures only “react” fires, but if both fire, you need the isCancelled/AbortController pattern. The combination of debouncing plus AbortController is the production-grade solution.The implementation: useDebounce hook on the query, useEffect that fetches on the debounced value, AbortController in the effect cleanup. This is so common that libraries like React Query handle all of it with a single useQuery({ queryKey: ['search', debouncedQuery], queryFn: ... }) call.