Side Effects Management
Side effects management with Hooks covers identifying external systems, cleanup, data fetching patterns, subscriptions, and keeping Effects focused.
Side effects are an essential part of real-world React applications.
A React component primarily describes what the UI should look like for the current state and props. But applications also need to interact with systems outside React, such as:
- APIs
- browser APIs
- timers
- event listeners
- WebSockets
- subscriptions
- local storage
- third-party libraries
- analytics systems
- DOM APIs
These interactions are called side effects.
React provides tools such as useEffect, event handlers, and other Hooks to manage them in a predictable way.
The goal is not to eliminate side effects.
The goal is to place side effects in the correct part of the application and keep them synchronized with the values they depend on.
What Is a Side Effect?
A side effect is work that reaches outside the component's rendering calculation or changes something external to React.
For example:
document.title = "Dashboard";
This changes the browser environment.
Another example:
localStorage.setItem("theme", "dark");
This changes browser storage.
An API request:
fetch("/api/products");
interacts with an external system.
A timer:
setTimeout(() => {// ...}, 1000);
also interacts with something outside React's rendering model.
Pure Rendering vs Side Effects
React rendering should be treated as a calculation.
Conceptually:
Props + State↓Render↓JSX
The same inputs should produce the same UI description.
Side effects are different:
React↓External System
Examples:
Component↓Browser API
Component↓Network
Component↓WebSocket
Keeping these responsibilities separate makes components easier to understand.
Common Side Effects
Real applications commonly perform side effects such as:
Network Requests
fetch("/api/products");
Browser APIs
window.addEventListener("resize", handleResize);
Document APIs
document.title = "Products";
Storage
localStorage.setItem("theme", theme);
Timers
setTimeout(callback, 1000);
Subscriptions
socket.subscribe(handleMessage);
Third-Party Libraries
const chart = new Chart(element, options);
Each needs an appropriate lifecycle and cleanup strategy.
The Main Tool: useEffect
useEffect allows a component to synchronize with an external system.
Basic syntax:
useEffect(() => {// setup or synchronizationreturn () => {// cleanup};}, [dependencies]);
The important parts are:
- setup logic
- optional cleanup logic
- dependency list
Effects Are About Synchronization
A useful way to think about an Effect is:
When these reactive values have these current values, make an external system match them.
For example:
useEffect(() => {document.title = title;}, [title]);
The Effect keeps the browser document title synchronized with the React value title.
This is more useful than thinking of Effects simply as "code that runs after rendering."
Effect Lifecycle
An Effect has a setup and optional cleanup.
useEffect(() => {const connection = connect();return () => {connection.disconnect();};}, []);
Conceptually:
Render↓Effect setup↓External system synchronized↓Dependencies change↓Cleanup previous Effect↓Setup next Effect↓Component unmounts↓Cleanup
Cleanup is important whenever the Effect creates something that needs to be stopped, removed, or disconnected.
Dependency Arrays
Dependencies tell React which reactive values the Effect uses.
Example:
useEffect(() => {document.title = title;}, [title]);
The Effect depends on title.
When title changes, React can synchronize the Effect again.
Another example:
useEffect(() => {connectToRoom(roomId);}, [roomId]);
The connection depends on roomId.
Empty Dependency Array
An Effect with an empty dependency array:
useEffect(() => {// setup}, []);
does not depend on changing reactive values from the component.
For a mounted component, this Effect's setup normally runs after the initial commit, with cleanup when the component is removed. In development Strict Mode, React may intentionally perform an extra setup/cleanup cycle to help reveal unsafe Effect logic.
Do not interpret [] as "run exactly once under every circumstance."
The important idea is that the Effect has no changing dependencies.
No Dependency Array
If you write:
useEffect(() => {// ...});
the Effect runs after every committed render.
This can be useful in specific synchronization scenarios, but it is often unnecessary.
If an Effect only needs to synchronize when particular values change, provide those dependencies.
Cleanup Functions
Cleanup stops or reverses the external work created by an Effect.
Example:
useEffect(() => {function handleResize() {console.log(window.innerWidth);}window.addEventListener("resize",handleResize);return () => {window.removeEventListener("resize",handleResize);};}, []);
The cleanup removes the event listener.
Without cleanup, repeated mounting or dependency changes can create unwanted listeners.
What Requires Cleanup?
Cleanup is commonly required for:
- event listeners
- subscriptions
- timers
- WebSocket connections
- observers
- external library instances
- pending operations that can be cancelled
Not every Effect needs cleanup.
For example:
useEffect(() => {document.title = title;}, [title]);
does not require cleanup because there is nothing to unsubscribe or disconnect.
Event Handlers vs Effects
One of the most important distinctions in React is deciding whether code belongs in an event handler or an Effect.
Consider:
function handleSubmit() {sendAnalyticsEvent("form_submitted");}
This is caused by a user interaction.
An event handler is usually the right place.
An Effect is appropriate when synchronization should happen because component state or props changed.
For example:
useEffect(() => {document.title = `${count} items`;}, [count]);
Simple rule
User interaction caused it↓Event handlerState/props changed and an external systemmust stay synchronized↓Effect
Do Not Use Effects for Derived Data
Suppose:
const [firstName, setFirstName] = useState("");const [lastName, setLastName] = useState("");
You might be tempted to write:
const [fullName, setFullName] = useState("");useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);
This is usually unnecessary.
fullName can be calculated during rendering:
const fullName = `${firstName} ${lastName}`;
The value is derived from existing state.
No external synchronization is needed.
Avoid Effect Chains
Avoid creating a chain of Effects that update state in sequence:
state A↓Effect↓state B↓Effect↓state C
This often makes the application harder to reason about.
Prefer deriving values when possible.
For example:
const filteredProducts = products.filter(...);const total = filteredProducts.reduce(...);
instead of storing both derived values and synchronizing them through Effects.
API Requests
Fetching data is a common Effect use case when the component needs to synchronize with an external data source.
A simplified example:
import { useEffect, useState } from "react";function Products() {const [products, setProducts] = useState([]);const [loading, setLoading] = useState(true);useEffect(() => {let ignore = false;async function loadProducts() {setLoading(true);const response = await fetch("/api/products");const data = await response.json();if (!ignore) {setProducts(data);setLoading(false);}}loadProducts();return () => {ignore = true;};}, []);if (loading) {return <p>Loading...</p>;}return (<ul>{products.map((product) => (<li key={product.id}>{product.name}</li>))}</ul>);}
The ignore pattern prevents an outdated request from updating state after the Effect has been cleaned up.
For production applications, dedicated data-fetching libraries can provide more complete caching, synchronization, retry, and request-management capabilities.
Handling Request Errors
Effects that perform asynchronous work should handle failures.
Example:
useEffect(() => {let ignore = false;async function loadProducts() {try {setLoading(true);setError(null);const response = await fetch("/api/products");if (!response.ok) {throw new Error("Request failed");}const data = await response.json();if (!ignore) {setProducts(data);}} catch (error) {if (!ignore) {setError(error);}} finally {if (!ignore) {setLoading(false);}}}loadProducts();return () => {ignore = true;};}, []);
This provides:
- loading state
- success state
- error state
- cleanup protection
AbortController
For fetch requests, an AbortController can actively cancel the request.
useEffect(() => {const controller = new AbortController();async function loadProducts() {try {const response = await fetch("/api/products",{signal: controller.signal,});const data = await response.json();setProducts(data);} catch (error) {if (error.name !== "AbortError") {setError(error);}}}loadProducts();return () => {controller.abort();};}, []);
This is useful when an in-flight request should be cancelled as the Effect is cleaned up.
Race Conditions
Race conditions can happen when multiple requests overlap.
Suppose:
User searches "rea"↓Request A startsUser quickly searches "react"↓Request B starts
If Request A finishes after Request B, it could overwrite the newer results.
A cleanup strategy or request cancellation can prevent stale results from winning.
For example:
useEffect(() => {const controller = new AbortController();async function search() {const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`,{signal: controller.signal,});const data = await response.json();setResults(data);}if (query) {search();}return () => {controller.abort();};}, [query]);
Each new query cleans up the previous request.
Browser Event Listeners
Effects are useful for subscribing to browser events.
useEffect(() => {function handleOnline() {setIsOnline(true);}function handleOffline() {setIsOnline(false);}window.addEventListener("online",handleOnline);window.addEventListener("offline",handleOffline);return () => {window.removeEventListener("online",handleOnline);window.removeEventListener("offline",handleOffline);};}, []);
The Effect owns the subscription lifecycle.
Timers
Timers are another common side effect.
useEffect(() => {const timer = setInterval(() => {setSeconds((value) => value + 1);}, 1000);return () => {clearInterval(timer);};}, []);
The cleanup prevents the interval from continuing after the component is no longer using it.
Local Storage
An Effect can synchronize React state with browser storage.
const [theme, setTheme] = useState("light");useEffect(() => {localStorage.setItem("theme",theme);}, [theme]);
The relationship is:
theme changes↓render↓Effect↓localStorage updated
If localStorage is used to initialize state, lazy state initialization can avoid unnecessary repeated reads:
const [theme, setTheme] = useState(() => {return localStorage.getItem("theme") ?? "light";});
WebSockets
A WebSocket connection is an external system that needs setup and cleanup.
useEffect(() => {const socket = new WebSocket(`wss://example.com/rooms/${roomId}`);socket.addEventListener("message",handleMessage);return () => {socket.removeEventListener("message",handleMessage);socket.close();};}, [roomId]);
When roomId changes:
Old room↓cleanup↓close old socket↓setup↓connect new room
This is exactly the kind of synchronization that Effects are designed for.
Third-Party Libraries
Suppose a chart library needs a DOM element.
function Chart({ data }) {const containerRef = useRef(null);useEffect(() => {const chart = createChart(containerRef.current,data);return () => {chart.destroy();};}, [data]);return <div ref={containerRef} />;}
The component owns the DOM element.
The Effect synchronizes the third-party library with React data.
Cleanup destroys the external instance.
Effect Dependencies
Dependencies should represent the reactive values used by the Effect.
Example:
useEffect(() => {connect(roomId, userId);}, [roomId, userId]);
Both roomId and userId affect the synchronization.
If one changes, the Effect should be reconsidered.
Avoid deliberately removing dependencies just to prevent an Effect from running.
Instead, restructure the logic when necessary.
Stale Closures
A stale closure occurs when an Effect or callback captures an older value.
For example:
useEffect(() => {const timer = setInterval(() => {console.log(count);}, 1000);return () => clearInterval(timer);}, []);
The Effect captures the value of count from the render where the Effect was created.
If the Effect needs the latest count, its dependency relationship must be designed correctly.
One option:
useEffect(() => {const timer = setInterval(() => {console.log(count);}, 1000);return () => clearInterval(timer);}, [count]);
Another option may be to use a functional state update when the actual requirement is to update state from the previous value:
setCount((value) => value + 1);
The correct solution depends on what the Effect is trying to synchronize.
Effects and Refs
Refs are useful when an Effect needs to interact with a DOM node or hold a mutable value.
Example:
function SearchInput() {const inputRef = useRef(null);useEffect(() => {inputRef.current?.focus();}, []);return <input ref={inputRef} />;}
The responsibilities are clear:
useRef↓access DOM elementuseEffect↓perform synchronization after commit
Effect Cleanup Timing
When dependencies change, React cleans up the previous Effect before running the new setup for the updated dependencies.
Conceptually:
Old Effect↓cleanup↓New Effect
When the component is removed:
Component unmount↓Effect cleanup
This is why cleanup should undo the external work created by that particular Effect.
Strict Mode and Effects
In development, React Strict Mode can intentionally run an additional setup/cleanup cycle for Effects.
For example:
setupcleanupsetup
This helps expose Effects that are not correctly reversible.
If an Effect creates a subscription:
useEffect(() => {subscribe();return () => {unsubscribe();};}, []);
the setup and cleanup should work correctly even when React exercises them more than once during development.
Do not add hacks simply to hide Strict Mode behavior.
Instead, make the Effect's setup and cleanup correct.
Effect Idempotency
Good Effects should be safe to start and clean up according to their lifecycle.
For example:
useEffect(() => {const connection = createConnection(roomId);connection.connect();return () => {connection.disconnect();};}, [roomId]);
Each setup creates one connection.
Each cleanup disconnects that connection.
This makes the lifecycle predictable.
Avoid Unnecessary Effects
A component does not need an Effect merely because something happens after rendering.
For example, this is unnecessary:
const [fullName, setFullName] = useState("");useEffect(() => {setFullName(`${firstName} ${lastName}`);}, [firstName, lastName]);
Prefer:
const fullName = `${firstName} ${lastName}`;
Similarly, if a button click should trigger an action:
function handleSave() {saveData();}
do not create an Effect just to watch a state variable representing the click.
Event Logic vs Effect Logic
Consider a purchase button.
Incorrect approach:
const [purchased, setPurchased] = useState(false);useEffect(() => {if (purchased) {sendPurchaseAnalytics();}}, [purchased]);function handleClick() {setPurchased(true);}
If the analytics event is specifically caused by the button click, the event handler can often express that relationship more directly:
function handleClick() {setPurchased(true);sendPurchaseAnalytics();}
Effects are best when the external system needs to stay synchronized with state or props, rather than when they are being used as a generic event system.
Effects and Derived Values
Avoid:
const [total, setTotal] = useState(0);useEffect(() => {setTotal(cart.reduce((sum, item) =>sum + item.price * item.quantity,0));}, [cart]);
Prefer:
const total = cart.reduce((sum, item) =>sum + item.price * item.quantity,0);
If the calculation is expensive, consider useMemo:
const total = useMemo(() => {return cart.reduce((sum, item) =>sum + item.price * item.quantity,0);}, [cart]);
The key distinction is:
Derived value↓calculate during renderExternal synchronization↓Effect
Effects and State Machines
Complex asynchronous behavior can become easier to reason about when state transitions are explicit.
For example:
const initialState = {status: "idle",data: null,error: null,};
A reducer can manage:
requestStartedrequestSucceededrequestFailed
An Effect can perform the external request:
Effect↓API request↓dispatch(action)↓reducer↓new state
This separates:
- external synchronization
- state transition logic
and works especially well with useReducer.
Custom Hooks for Side Effects
When side-effect logic becomes reusable, extract it into a Custom Hook.
For example:
function useOnlineStatus() {const [isOnline, setIsOnline] = useState(navigator.onLine);useEffect(() => {function handleOnline() {setIsOnline(true);}function handleOffline() {setIsOnline(false);}window.addEventListener("online",handleOnline);window.addEventListener("offline",handleOffline);return () => {window.removeEventListener("online",handleOnline);window.removeEventListener("offline",handleOffline);};}, []);return isOnline;}
Now components can simply write:
const isOnline = useOnlineStatus();
This keeps component code focused on UI.
A Practical Decision Framework
When you need to perform some logic, ask:
Question 1: Is it a pure calculation?
If yes, calculate it during rendering.
const total = price * quantity;
Question 2: Is it caused directly by a user interaction?
If yes, use an event handler.
function handleClick() {submitForm();}
Question 3: Does React state or props need to stay synchronized with an external system?
If yes, consider an Effect.
useEffect(() => {externalSystem.update(value);}, [value]);
Question 4: Does it need setup and cleanup?
If yes, an Effect is often appropriate.
useEffect(() => {subscribe();return () => {unsubscribe();};}, []);
Question 5: Is the logic reused across components?
If yes, consider a Custom Hook.
Common Mistakes
1. Using Effects for Everything
Effects are not a replacement for event handlers or normal calculations.
2. Missing Dependencies
If an Effect uses a reactive value, its dependency relationship needs to be represented correctly.
3. Suppressing Dependency Warnings
Avoid silencing lint warnings without understanding the underlying dependency problem.
Often the better solution is to restructure the Effect.
4. Forgetting Cleanup
Subscriptions, listeners, timers, and external resources often require cleanup.
5. Updating State in an Unnecessary Effect
Avoid creating state solely for values that can be derived.
6. Creating Effect Chains
Prefer direct calculations and explicit state transitions over long chains of Effects.
7. Ignoring Race Conditions
Asynchronous Effects can overlap.
Use cancellation or cleanup strategies when older work must not overwrite newer results.
8. Assuming [] Means "Never Again"
An empty dependency array means the Effect has no changing dependencies. Development Strict Mode may still exercise setup and cleanup more than once.
9. Performing Side Effects During Rendering
Avoid:
function Component() {document.title = "Dashboard";return <h1>Dashboard</h1>;}
Prefer:
function Component() {useEffect(() => {document.title = "Dashboard";}, []);return <h1>Dashboard</h1>;}
Best Practices
1. Keep Rendering Pure
Rendering should primarily calculate UI.
2. Use Event Handlers for User Actions
If a user clicks a button, the handler is usually the clearest place for the resulting action.
3. Use Effects for External Synchronization
Use Effects for subscriptions, browser APIs, network synchronization, and similar external systems.
4. Keep Effects Small
One Effect should ideally represent one coherent synchronization concern.
5. Always Clean Up What You Set Up
If an Effect creates:
listenertimersubscriptionconnectionobserverexternal instance
consider how and when it should be cleaned up.
6. Keep Dependencies Accurate
Do not treat dependency arrays as a performance switch.
They describe the reactive values the Effect depends on.
7. Avoid Derived State
Calculate values during rendering whenever possible.
8. Handle Async Work Carefully
Consider:
- errors
- cancellation
- race conditions
- loading states
- stale results
9. Extract Reusable Effect Logic
Use Custom Hooks when the same side-effect behavior appears in multiple components.
10. Test Behavior, Not Implementation Details
Verify observable outcomes such as:
- subscriptions are created
- cleanup happens
- requests are cancelled when appropriate
- state updates correctly
- external systems receive the expected values
Practical Example: Complete Search Component
import {useEffect,useState,} from "react";function Search() {const [query, setQuery] = useState("");const [results, setResults] = useState([]);const [loading, setLoading] = useState(false);const [error, setError] = useState(null);useEffect(() => {if (!query.trim()) {setResults([]);setLoading(false);setError(null);return;}const controller = new AbortController();async function search() {try {setLoading(true);setError(null);const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`,{signal: controller.signal,});if (!response.ok) {throw new Error("Search failed");}const data = await response.json();setResults(data);} catch (error) {if (error.name !== "AbortError") {setError(error);}} finally {if (!controller.signal.aborted) {setLoading(false);}}}search();return () => {controller.abort();};}, [query]);return (<div><inputvalue={query}onChange={(event) =>setQuery(event.target.value)}placeholder="Search..."/>{loading && <p>Searching...</p>}{error && (<p>Unable to complete the search.</p>)}<ul>{results.map((result) => (<li key={result.id}>{result.name}</li>))}</ul></div>);}export default Search;
This example demonstrates:
- state
- Effect synchronization
- dependency tracking
- request cancellation
- error handling
- loading state
- cleanup
- conditional rendering
Practical Example: Subscription Management
function ChatRoom({ roomId }) {const [messages, setMessages] = useState([]);useEffect(() => {const connection = createChatConnection(roomId);function handleMessage(message) {setMessages((current) => [...current,message,]);}connection.on("message", handleMessage);connection.connect();return () => {connection.off("message", handleMessage);connection.disconnect();};}, [roomId]);return (<div>{messages.map((message) => (<p key={message.id}>{message.text}</p>))}</div>);}
When roomId changes:
Old room↓remove listener↓disconnect↓new room↓connect↓subscribe
This is a textbook synchronization pattern.
Practice Exercises
Exercise 1: Document Title
Create a component that keeps:
document.title
synchronized with a count state.
Exercise 2: Window Resize
Build a component that tracks:
window.innerWidthwindow.innerHeight
Use an Effect and clean up the resize listener.
Exercise 3: Timer
Build a timer that:
- starts an interval
- increments elapsed seconds
- cleans up the interval
- can be started and stopped
Exercise 4: Local Storage
Synchronize a theme setting with:
localStorage
Initialize the state from storage and save changes back to storage.
Exercise 5: API Request
Build a product component that:
- fetches products
- displays loading
- handles errors
- displays results
- cancels stale requests
Exercise 6: WebSocket
Create a Chat Room component that:
- connects to a room
- listens for messages
- displays messages
- disconnects when the room changes or component unmounts
Exercise 7: Refactor an Unnecessary Effect
Given:
const [total, setTotal] = useState(0);useEffect(() => {setTotal(cart.reduce((sum, item) =>sum + item.price * item.quantity,0));}, [cart]);
Refactor it so total is derived without an Effect.
Mini Project: Online Dashboard
Build an Online Dashboard that combines multiple side-effect patterns.
Requirements:
1. Online Status
Display:
OnlineOffline
Use browser online/offline events.
2. Document Title
Keep the title synchronized:
Dashboard - OnlineDashboard - Offline
3. Data Fetching
Fetch dashboard data from an API.
Handle:
LoadingSuccessError
4. Window Size
Display the current browser dimensions.
5. Cleanup
Correctly clean up:
- event listeners
- timers if used
- pending requests
- subscriptions
Suggested architecture:
Dashboard│├── useOnlineStatus()│├── useEffect()│ └── document.title│├── useEffect()│ └── API request│└── useWindowSize()
This project demonstrates how multiple independent side-effect concerns can coexist without turning the component into one giant Effect.
Interview questions & answers
Interview preparation
9 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is a side effect in React?
BeginnerModel answer
A side effect is an interaction with something outside the component's rendering calculation, such as an API, browser API, timer, subscription, storage system, or third-party library.
2Why does an Effect need cleanup?
BeginnerModel answer
Cleanup prevents resources created by the Effect from continuing after the synchronization is no longer needed. Examples:remove event listener → clear timer → unsubscribe → disconnect WebSocket → destroy library instance → cancel request3Why should you avoid derived state in Effects?
BeginnerModel answer
Because derived values can usually be calculated directly during rendering.
Using an Effect introduces an unnecessary extra state update and can create synchronization complexity.
Intermediate
4When should you use useEffect?
IntermediateModel answer
UseuseEffectwhen a component needs to synchronize with an external system. Examples include:- subscriptions
- browser APIs
- network synchronization
- timers
- third-party libraries
5What does the dependency array do?
IntermediateModel answer
It describes the reactive values that the Effect depends on. Example:useEffect(() => { → connect(roomId); → }, [roomId]);The Effect is reconsidered whenroomIdchanges.6What is a stale closure?
IntermediateModel answer
A stale closure occurs when a callback or Effect uses values captured from an older render rather than the latest values.
Correct dependencies, functional state updates, refs, or restructuring the logic can solve different stale-value scenarios.
Advanced
7Should every asynchronous operation use useEffect?
AdvancedModel answer
No.
The correct location depends on what causes the operation.
If it is directly caused by a user interaction, an event handler may be more appropriate.
If it synchronizes with changing props or state, an Effect may be appropriate.
8What is an Effect cleanup function?
AdvancedModel answer
It is the function returned from the Effect:useEffect(() => { → subscribe(); → return () => { → unsubscribe(); → }; → }, []);React uses it to clean up the previous synchronization.9What happens to Effects in Strict Mode?
AdvancedModel answer
In development, Strict Mode may intentionally run an additional setup/cleanup cycle to help identify Effects that do not correctly handle their lifecycle.
Side Effects Cheat Sheet
Synchronize document title
useEffect(() => {document.title = title;}, [title]);
Subscribe
useEffect(() => {subscribe();return () => {unsubscribe();};}, []);
Event listener
useEffect(() => {window.addEventListener("resize",handleResize);return () => {window.removeEventListener("resize",handleResize);};}, []);
Timer
useEffect(() => {const timer = setInterval(callback, 1000);return () => {clearInterval(timer);};}, []);
Fetch with cancellation
useEffect(() => {const controller = new AbortController();fetch(url, {signal: controller.signal,});return () => {controller.abort();};}, [url]);
Derived value
Avoid:
useEffect(() => {setTotal(calculateTotal(items));}, [items]);
Prefer:
const total = calculateTotal(items);
User interaction
Prefer:
function handleClick() {submitForm();}
rather than using an Effect to detect that a click happened.