React components are meant to be pure โ they return UI from props and state without touching the outside world. But real apps need to fetch data, subscribe to events, and manage timers, and useEffect is the hook that lets you do that safely, without breaking React's rendering model.
1Side Effects
React components are meant to be pure functions โ they take props and return UI without touching anything outside their own scope. But real applications need to interact with the outside world: fetching data from an API, subscribing to a WebSocket, or manually manipulating the DOM. React calls these external interactions Side Effects.
// Side Effects: Interacting with the outside worldConnecting to External API... ๐ก
2The useEffect Hook
The useEffect Hook is React's built-in way to run side effects safely inside a function component. It takes two arguments: a callback function containing your effect logic, and an optional dependency array controlling when that callback runs โ acting as an escape hatch for synchronizing your component with external systems.
import { useEffect } from 'react';
useEffect(() => {
console.log('Effect running!');
}, []);3The Dependency Array
The dependency array is the second argument to useEffect and controls exactly when React re-runs your callback. Passing an empty array ([]) tells React the effect depends on nothing, so it runs exactly once, right after the component's first render โ ideal for one-time work like an initial data fetch.
useEffect(() => {
fetchData();
}, []); // Runs onceComponent Mounted
Effect Triggered: 1 time
4Tracking State Changes
Placing variables โ state or props โ inside the dependency array turns the effect into a listener for those specific values. On every render, React compares each dependency against its value from the previous render, and only re-runs the callback if at least one of them actually changed.
useEffect(() => {
document.title = `Count: `;
}, [count]); // Runs when count changesCount: 5
5Omitting the Array
It's technically possible to omit the dependency array entirely, passing only the callback function. Doing so tells React the effect depends on everything, so it runs after every single render โ a pattern that's rare in practice and a common source of performance problems and infinite loops, so use it with extreme caution.
useEffect(() => {
console.log('I run every time!');
}); // No arrayEffect triggering constantly!
6Cleanup Functions
Effects that start ongoing processes โ like intervals, WebSocket connections, or event listeners โ can return a Cleanup Function. React automatically calls this function right before the component unmounts, or right before the effect runs again, giving you a place to tear down whatever the effect set up.
useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => clearInterval(timer); // Cleanup
}, []);7Memory Leaks
If an effect adds a 'scroll' listener to the window and the component unmounts without removing it, that listener keeps living in memory โ and if the user navigates back and the component mounts again, a second listener stacks on top of it. This accumulation of abandoned processes is a memory leak, and a proper cleanup function is what prevents it.
/* Effect Lab: API Simulation & Cleanup */Event Listener Active
Will be safely removed on unmount.
8Infinite Loops
One of the most dangerous mistakes with useEffect is creating an infinite loop: the effect updates a piece of state, and that same state is listed in its own dependency array. The cycle becomes render, run effect, update state, re-render, run effect again โ and it will crash the browser tab almost instantly.
useEffect(() => {
setCount(count + 1); // โ INFINITE LOOP
}, [count]);๐ฅ Browser Crashed ๐ฅ
9Timing: Paint vs Effect
React calculates UI changes, updates the DOM, and lets the browser physically paint the screen before it runs your effect callback. That ordering means side effects never block the visual rendering of the page, so a data fetch or subscription inside an effect won't delay what the user sees on screen.
// Render -> Browser Paint -> Effect runsUI Visible Immediately
(Data fetching happens in background)
10Synchronization Tool
Experienced React developers avoid thinking of useEffect as a lifecycle hook like the old componentDidMount. Instead, its real purpose is synchronization: keeping some external system โ LocalStorage, a remote database, a browser API โ in sync with your component's current state, whatever that state happens to be at the time.
/* Syncing with LocalStorage Example */
useEffect(() => {
localStorage.setItem('val', val);
}, [val]);11Effects vs. Event Handlers
A common source of confusion is treating 'code that reacts to something' as interchangeable with 'code that belongs in useEffect'. If you're responding to a specific user interaction โ a click, a form submission, a key press โ that logic belongs in an event handler, not an effect. Event handlers fire because of a particular interaction; effects fire because the component rendered, or one of its dependencies changed, regardless of what (if anything) caused that change.
function Form() {
function handleSubmit() {
postData(); // Event: fires on submit
}
}12You Might Not Need an Effect
One of the most common React mistakes is storing a value in state and using useEffect to keep it in sync, when that value could simply be calculated during render. If a total can be derived from price and quantity, don't create a 'total' state and update it inside an effect โ just compute const total = price * qty directly in the component body. This avoids an unnecessary extra render and keeps the data flow easy to follow.
// โ Unnecessary effect
useEffect(() => setTotal(price * qty), [price, qty]);
// โ
Just calculate it
const total = price * qty;$49.99
13Exhaustive Dependencies
The Exhaustive Dependencies rule states that every variable, function, or piece of state used inside the effect callback must be listed in its dependency array. Leaving one out means the effect closes over a stale, outdated version of that value from an earlier render โ which is why it's worth trusting the React ESLint plugin's dependency warnings rather than silencing them.
const name = 'Lolly';
useEffect(() => {
console.log(name);
}, [name]); // Include dependencies!ESLint: Passing โ
All dependencies are exhaustively listed.
14Objects, Arrays & Referential Equality
React decides whether a dependency 'changed' using Object.is, which for objects and arrays compares reference, not content. Writing const config = { id } inside a component body creates a brand new object on every render โ even when id stays the same โ so putting that object straight into a dependency array makes the effect re-run every time, because { id: 5 } is never === to another { id: 5 }. Prefer depending on the raw primitive value itself.
// โ New object every render โ effect re-runs constantly
useEffect(() => {...}, [{ id }]);
// โ
Depend on the primitive
useEffect(() => {...}, [id]);...because a new object was created on every render
15Race Conditions in Data Fetching
When an effect fetches data and a dependency (like userId) changes before the first request finishes, you can get a race condition: the old request can resolve after the new one, overwriting fresh data with stale data. The fix is to track whether the effect has been superseded โ using a local 'ignore' flag set inside the cleanup function, or cancelling the request with an AbortController โ so a stale response is simply discarded instead of applied to state.
useEffect(() => {
let ignore = false;
fetchUser(id).then(data => { if (!ignore) setUser(data); });
return () => { ignore = true; };
}, [id]);16Functions as Dependencies
A function declared inside a component body is a brand new function on every render, just like an object literal. If that function is used inside an effect and listed as a dependency, the effect re-runs on every render unless you stabilize its identity. useCallback can freeze a function's reference across renders โ but reach for it specifically to solve this dependency problem, not as a default habit on every function you write.
// โ New function reference every render
function load() { fetchData(id); }
useEffect(() => load(), [load]);
// โ
Stabilized with useCallback
const load = useCallback(() => fetchData(id), [id]);Effect only re-runs when 'id' changes
17Combining useEffect with useRef
Sometimes an effect needs to reach directly into the DOM โ to focus an input, measure an element, or call an imperative method on a video player. useRef gives you a stable handle to the actual DOM node, and because updating a ref never triggers a re-render, refs pair naturally with effects: the effect waits until after render, when the DOM node is guaranteed to exist, then reads or manipulates ref.current directly.
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);18The Golden Question
Before reaching for useEffect, ask one question: 'Am I synchronizing this component with something outside React?' If yes โ a browser API, a subscription, a timer, a third-party widget, a network request โ useEffect is the right tool. If you're only calculating a value from existing props and state, or responding to a specific user interaction, you almost certainly don't need an effect at all.
// Am I syncing with something external?
// YES -> useEffect
// NO -> calculate during render, or use an event handlerExternal system?
API ยท DOM ยท Timer ยท Subscription โ useEffect
19Mastery Achieved
Bringing this together: useEffect lets you safely reach outside a component to fetch data, subscribe to external systems, and clean up after yourself when a component unmounts or its dependencies change. Getting the dependency array right โ and knowing when NOT to use an effect at all โ is what separates an effect that behaves predictably from one that causes stale data, memory leaks, race conditions, or runaway re-renders.
/* Next: Conditional Logic */Side Effects Mastered โ
20Step-by-Step Breakdown
Side Effects. Welcome to the Effect Lifecycle. In React, components are intended to be 'pure' functionsโthey take props and return UI without altering anything outside their scope. However, real applications require interaction with the outside world, such as fetching data from an API, subscribing to websockets, or manually manipulating the DOM. In React terminology, these external interactions are called 'Side Effects'.
The useEffect Hook. To safely execute Side Effects in a functional component, React provides the useEffect Hook. This hook is a built-in function that takes two arguments: a callback function (where your effect logic lives) and an optional dependency array (which controls when the effect runs). useEffect acts as an escape hatch, allowing you to synchronize your component with external systems.
The Dependency Array. The dependency array is the second argument to useEffect, and it serves as the 'brain' of the effect. It dictates exactly when React should execute your callback. If you provide an EMPTY array ([]), you are telling React: 'this effect depends on absolutely nothing.' As a result, the effect will only run EXACTLY ONCE when the component first mounts (is added to the DOM), making it perfect for initial data fetching.
When passing an empty dependency array [] as the second argument to useEffect, when will the internal callback execute?
- โOn every single render
- โOnly once when the component mounts
Tracking State Changes. If you place variables (like state or props) inside the dependency array, you turn the effect into a listener. React will compare the current values of those variables against their previous values on every render. Ifโand only ifโany variable in the array has changed, React will execute the effect again. This is how you synchronize external systems with specific state changes.
Omitting the Array. It is technically possible to omit the dependency array entirely by passing only the callback function. If you do this, React assumes the effect depends on everything, and will execute the effect after EVERY single render of the component. This is extremely rare in modern React and is often a source of severe performance issues and infinite loops. Use it with extreme caution.
Cleanup Functions. Many side effects create ongoing processes, such as intervals, WebSocket connections, or DOM event listeners. If a component unmounts while these processes are running, it causes a memory leak. To prevent this, your effect can return a Cleanup Function. React will automatically execute this cleanup function right before the component unmounts, or right before the effect re-runs.
Memory Leaks. Imagine an effect that adds a 'scroll' listener to the global window object. If the user navigates away and the component is destroyed, that listener still exists in browser memory. If the user returns, a SECOND listener is added. This accumulation of abandoned processes is a Memory Leak. The cleanup function guarantees that external subscriptions are cleanly destroyed when no longer needed.
To safely remove an event listener or stop a timer when a component is destroyed (unmounted), what must your effect do?
- โReturn a Cleanup Function
- โCall the React.destroy() method
Infinite Loops. One of the most dangerous mistakes you can make is creating an infinite loop. This happens when your effect updates a piece of state, and that same state is listed in the dependency array. The sequence goes: Render -> Effect runs -> State updates -> Component Re-renders -> Effect runs -> State updates... This will crash the browser tab instantly.
Timing: Paint vs Effect. It is crucial to understand the timing of useEffect. React will calculate the UI changes, update the DOM, and allow the browser to physically paint the screen BEFORE it executes your effect. This means your side effects will never block the visual rendering of the page, ensuring a fast, non-blocking user experience.
When a component containing a useEffect hook is removed from the screen, which phase of the lifecycle does React execute to clean up resources?
- โThe Mount phase
- โThe Unmount phase
Synchronization Tool. Advanced React developers don't think of useEffect as a 'lifecycle hook' (like componentDidMount). Instead, they treat it strictly as a synchronization tool. Its only purpose is to synchronize the React state with an external system. Whether you are syncing state to LocalStorage, a remote database, or a 3D canvas, the mental model is 'Sync', not 'Lifecycle'.
Effects vs. Event Handlers. A common source of confusion is mixing up 'code that reacts to something' with 'code that belongs in useEffect'. If you are responding to a specific user interactionโa click, a form submission, a key pressโthat logic belongs in an EVENT HANDLER, not an effect. Event handlers fire because of a particular interaction. Effects fire because the component was displayed, or because one of its dependencies changed, no matter what (if anything) caused that change.
You Might Not Need an Effect. One of the most common React mistakes is storing a value in state and then using useEffect to keep it in sync, when that value could simply be CALCULATED during render. If a total can be derived from price and quantity, don't create a 'total' state and update it inside an effectโjust compute 'const total = price * quantity' directly in the component body. This avoids an unnecessary extra render and keeps your data flow easy to follow.
A component needs to display fullName, computed from firstName and lastName. What is the best approach?
- โStore fullName in state and update it inside a useEffect
- โCalculate const fullName =
${firstName} ${lastName}during render
Exhaustive Dependencies. A strict rule in modern React is the Exhaustive Dependencies rule. Every single variable, function, or piece of state that is used INSIDE the effect MUST be declared inside the dependency array. If you fail to include a variable, your effect will 'see' an old, stale version of that variable from a previous render (a stale closure). Always trust the React ESLint warnings regarding dependencies.
Objects, Arrays & Referential Equality. React decides whether a dependency 'changed' using Object.is, which for objects and arrays compares REFERENCE, not content. If you write 'const config = { id }' inside your component body, a brand new object is created on every single renderโeven when 'id' stays the same. Putting that object straight into a dependency array makes the effect re-run every time, because { id: 5 } is never === to another { id: 5 }. Prefer depending on the raw primitive value itself.
Race Conditions in Data Fetching. When an effect fetches data and a dependency (like userId) changes before the first request finishes, you can get a 'race condition': the OLD request can resolve AFTER the new one, overwriting fresh data with stale data. The fix is to track whether the effect has been supersededโusing a local 'ignore' flag set inside the cleanup function, or cancelling the request with an AbortControllerโso a stale response is simply discarded instead of applied to state.
In a fetch effect, why do we check an ignore flag before calling setUser(data)?
- โTo discard a stale response if a newer request already started
- โTo make the network request run faster
Functions as Dependencies. A function declared inside your component body is a brand new function on every render, just like an object literal. If that function is used inside an effect and listed as a dependency, the effect re-runs on every render unless you stabilize its identity. useCallback can freeze a function's reference across rendersโbut reach for it specifically to solve THIS dependency problem, not as a default habit on every function you write.
Combining useEffect with useRef. Sometimes an effect needs to reach directly into the DOMโto focus an input, measure an element, or call an imperative method on a video player. useRef gives you a stable handle to the actual DOM node, and because updating a ref never triggers a re-render, refs pair naturally with effects: the effect waits until after render (when the DOM node is guaranteed to exist), then reads or manipulates ref.current directly.
The Golden Question. Before reaching for useEffect, ask ONE question: 'Am I synchronizing this component with something OUTSIDE React?' If yesโa browser API, a subscription, a timer, a third-party widget, a network requestโuseEffect is the right tool. If you're only calculating a value from existing props and state, or responding to a specific user interaction, you almost certainly don't need an effect at all.
Mastery Achieved. Effect mastery achieved! You now understand how to securely connect React to the outside world. You can handle mounting, synchronization, memory cleanups, and avoid fatal infinite loops. You are ready to manage complex state architectures and data fetching in enterprise applications.
Level Up ๐
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Manage Focus Inside an Effect After Route or View Changes
When a useEffect runs after a route change or a modal opens, it's a common place to imperatively move focus (e.g., ref.current.focus()) to the new heading or dialog, since screen reader users otherwise stay focused on content that's no longer relevant.
2Clean Up Live Region Announcements You Set Up in an Effect
If an effect updates an aria-live region or subscribes to a notification stream, its cleanup function should clear or unsubscribe from that live region so stale announcements aren't left active after the component unmounts.
SEO Implications
- 1
Data Fetched Inside useEffect Is Invisible to Crawlers That Don't Execute JavaScript
Because useEffect only runs after the initial render (and after hydration on the client), content loaded inside it is absent from the raw server-rendered HTML โ crawlers or previews that don't run JavaScript see the page before that data ever arrives.
- 2
Prefer Server-Side or Static Data Fetching for SEO-Critical Content
If content needs to be indexed reliably, fetch it during server rendering or at build time rather than exclusively inside a client-side useEffect, so the first HTML response already contains it.
Best Practices
Always Return a Cleanup Function for Subscriptions and Timers
Any effect that starts an interval, adds an event listener, or opens a subscription should return a function that reverses it โ otherwise the process keeps running after the component unmounts, leaking memory.
Trust the Exhaustive-Deps ESLint Rule Instead of Suppressing It
Disabling the react-hooks/exhaustive-deps warning to silence it usually just hides a stale closure bug rather than fixing it โ it's almost always safer to include the dependency and restructure the effect if it re-runs too often.
Calculate Derived Values During Render Instead of Syncing Them With an Effect
If a value can be computed directly from existing props or state (a total, a filtered list, a formatted string), calculate it inline in the component body rather than storing it in its own state and updating that state from a useEffect โ the effect version costs an extra render and is a common source of stale-value bugs.
Guard Async Effects Against Race Conditions
When an effect's callback is asynchronous and its dependencies can change before the request resolves, track staleness with a local 'ignore' flag (or an AbortController) inside the cleanup function so a slower, superseded response can never overwrite state set by a newer one.
Depend on Primitives, Not Freshly-Created Objects or Arrays
An object or array literal created inside the component body is a new reference on every render, so listing it as a dependency makes the effect re-run every time regardless of whether its contents actually changed โ depend on the underlying primitive values instead.
Frequent Bugs
A component's tab title, timer, or logged value keeps showing an old, outdated piece of state.
A variable used inside the effect was left out of the dependency array, so the effect closed over a stale value from an earlier render. Add the missing dependency so the effect re-runs when it changes.
The browser tab freezes or crashes shortly after a component mounts.
The effect updates a state variable that is also listed in its own dependency array, creating an infinite render loop. Remove the unnecessary dependency, or restructure the update so it doesn't re-trigger the same effect.
A fetch-driven effect re-runs on every render even though the requested resource hasn't changed.
The dependency array holds an object or array literal recreated on every render (e.g. `[{ id }]` instead of `[id]`). Depend on the primitive value directly so Object.is sees it as unchanged when it truly hasn't.
After rapidly switching between two records, the UI briefly shows the wrong one's data.
A slower request for the first record resolved after a faster request for the second, overwriting the correct data. Add an 'ignore' flag (or AbortController) set in the effect's cleanup function so a stale response is discarded instead of applied.
Real-World Examples
Fetching Data Once on Mount
A profile page fetches a user's data from an API exactly once when the component first appears, guarding against a race condition if the component unmounts before the request resolves.
useEffect(() => {
let ignore = false;
fetchUser(userId).then((data) => {
if (!ignore) setUser(data);
});
return () => { ignore = true; };
}, [userId]);Auto-Focusing a Form Field
A search bar should have its input focused as soon as it mounts. useRef supplies a stable handle to the actual DOM node, and the effect calls focus() on it once render has committed.
const searchRef = useRef(null);
useEffect(() => {
searchRef.current?.focus();
}, []);