React components are mainly responsible for describing what the user interface should look like for a particular state and set of props. However, real applications often need to do things that are not simply about rendering UI.
For example, an application may need to:
- Fetch data from an API
- Start or stop a timer
- Subscribe to browser events
- Listen for changes from an external service
- Update the browser document title
- Connect to a WebSocket
- Read information from a browser API
- Clean up resources when a component is no longer needed
These operations are called side effects.
React provides the useEffect Hook for synchronizing a component with something outside of React.
Understanding Effects is important because useEffect is powerful, but it is also one of the Hooks that beginners commonly misuse. Many pieces of code that appear to need an Effect can actually be handled directly during rendering or inside an event handler.
In this chapter, you will learn what Effects are, how useEffect works, how dependency arrays control when Effects run, how cleanup functions work, how to fetch data and use timers, how to work with subscriptions and browser APIs, and how to avoid common useEffect mistakes.
What Is an Effect in React?
An Effect is code that synchronizes a React component with something outside the component’s normal rendering process.
Examples include:
- Connecting to a server
- Fetching information from an external API
- Setting up a browser event listener
- Starting a timer
- Updating an external library
- Subscribing to a data source
- Changing a browser API value
The important idea is that Effects are about synchronization with external systems.
Consider this component:
function Welcome() {
return <h1>Welcome to Web4Learners</h1>;
}
This component only describes the UI. It does not need an Effect.
Now imagine that the component needs to listen for the browser’s online/offline status:
useEffect(() => {
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
The browser is an external system from React’s point of view.
The Effect connects the component to that external system and the cleanup function disconnects it.
What Does useEffect Do?
useEffect is a React Hook that allows a component to perform an Effect after React has rendered the component.
You import it from React:
import { useEffect } from "react";
A basic Effect looks like this:
useEffect(() => {
console.log("Effect ran");
});
The function passed to useEffect is called the setup function.
React runs this setup function after the component has been committed to the DOM.
A more realistic example is:
import { useEffect } from "react";
function PageTitle() {
useEffect(() => {
document.title = "Web4Learners";
});
return <h1>Welcome to Web4Learners</h1>;
}
Here, React renders the heading, and the Effect updates the browser tab title.
Why Does React Need Effects?
React rendering should ideally remain predictable.
Given the same props and state, a component should describe the same UI.
For example:
function UserProfile({ name }) {
return <h1>Hello, {name}</h1>;
}
Rendering this component does not need to communicate with anything outside React.
But consider:
document.title = "User Profile";
This changes something outside React’s rendered UI.
Or:
window.addEventListener("resize", handleResize);
This creates a relationship with the browser.
Or:
fetch("/api/users");
This communicates with an external server.
These are examples of operations that may need an Effect.
The Basic useEffect Syntax
The general syntax is:
useEffect(() => {
// Effect code
return () => {
// Cleanup code
};
}, [dependencies]);
There are three important parts:
Setup Function
The first argument is the setup function:
useEffect(() => {
console.log("Effect started");
});
Cleanup Function
The setup function can optionally return another function:
useEffect(() => {
console.log("Effect started");
return () => {
console.log("Effect cleaned up");
};
});
The returned function is called the cleanup function.
Dependency Array
The second argument is the dependency array:
useEffect(() => {
console.log("Effect ran");
}, [userId]);
The dependency array tells React which reactive values the Effect depends on.
When Does an Effect Run?
The behavior of an Effect depends largely on whether you provide a dependency array.
There are three common patterns.
Effect Without a Dependency Array
useEffect(() => {
console.log("Effect ran");
});
Without a dependency array, the Effect runs after every completed render.
For example:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Rendered");
});
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Every state update causes a render, so the Effect runs after each render.
This can be useful in specific situations, but it is often unnecessary.
Effect With an Empty Dependency Array
useEffect(() => {
console.log("Effect ran");
}, []);
The empty dependency array means the Effect does not depend on any reactive values from the component.
It generally runs after the initial mount.
For example:
useEffect(() => {
document.title = "Web4Learners";
}, []);
This is commonly used when setting up something that only needs to be initialized for the component’s lifetime.
However, an empty dependency array should not be added simply to “make an Effect run once.” The Effect should accurately represent what it depends on.
Effect With Dependencies
useEffect(() => {
console.log("User changed:", userId);
}, [userId]);
React runs the Effect after the initial render and again when userId changes.
For example:
function UserProfile({ userId }) {
useEffect(() => {
console.log("Load user:", userId);
}, [userId]);
return <h1>User Profile</h1>;
}
If the parent passes a different userId, the Effect synchronizes with the new user.
Understanding the Dependency Array
The dependency array is one of the most important parts of useEffect.
Consider:
useEffect(() => {
console.log(userId);
}, [userId]);
The Effect uses userId, so userId belongs in the dependency array.
Another example:
useEffect(() => {
console.log(userId);
console.log(theme);
}, [userId, theme]);
The Effect depends on both values.
React compares dependency values between renders. When one of them changes, React runs the cleanup from the previous Effect, if there is one, and then runs the new setup.
Why Dependencies Matter
Suppose you write:
useEffect(() => {
console.log(userId);
}, []);
If userId can change during the component’s lifetime, this Effect does not correctly describe that dependency.
The Effect may continue using the value from the render where it was created.
Instead, use:
useEffect(() => {
console.log(userId);
}, [userId]);
This tells React:
Synchronize this Effect whenever the user ID changes.
Effects and Props
Effects can depend on props.
function UserProfile({ userId }) {
useEffect(() => {
console.log("Loading user:", userId);
}, [userId]);
return <h1>User Profile</h1>;
}
If the parent changes userId, React renders the component with the new value and runs the Effect again.
Effects and State
Effects can also depend on state.
function SearchPage() {
const [query, setQuery] = useState("");
useEffect(() => {
console.log("Search query changed:", query);
}, [query]);
return (
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
/>
);
}
Every time query changes, the Effect synchronizes with the new value.
Cleanup Functions
Some Effects create resources or relationships that need to be removed.
Examples include:
- Event listeners
- Timers
- Subscriptions
- WebSocket connections
- External library connections
For these situations, an Effect can return a cleanup function.
useEffect(() => {
// Setup
return () => {
// Cleanup
};
}, []);
The cleanup function should undo or disconnect what the setup function created.
Why Cleanup Is Important
Consider an event listener:
useEffect(() => {
window.addEventListener("resize", handleResize);
}, []);
If the component is removed, the event listener can remain attached unless it is removed.
A better implementation is:
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
The Effect establishes the connection, and the cleanup removes it.
This pattern is extremely important:
Setup what you need. Clean up what you created.
When Does Cleanup Run?
Cleanup runs before the Effect is re-run because its dependencies changed.
For example:
useEffect(() => {
console.log("Connect:", roomId);
return () => {
console.log("Disconnect:", roomId);
};
}, [roomId]);
If roomId changes:
Previous Effect cleanup
↓
New Effect setup
When the component is removed from the screen, the final cleanup also runs.
This makes Effects useful for synchronizing external resources with the component’s current state.
A Subscription Example
Imagine a component subscribing to a chat room:
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
When roomId changes, React disconnects from the old room and connects to the new room.
This is much safer than continuously creating new connections without removing the old ones.
React Strict Mode and Cleanup
When developing with React Strict Mode, you may sometimes see an Effect setup and cleanup happen more than once during development.
For example:
Setup
Cleanup
Setup
This development behavior helps reveal Effects that do not correctly clean up after themselves.
It does not mean your production application will necessarily perform the same sequence.
If an Effect breaks when setup and cleanup happen in sequence, that usually indicates that the Effect needs better synchronization or cleanup logic.
Fetching Data with useEffect
Fetching data is a common reason developers use useEffect.
For example:
import { useEffect, useState } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch("/api/users")
.then((response) => response.json())
.then((data) => {
setUsers(data);
});
}, []);
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
The Effect starts the request after the component renders.
When the request completes, the data is stored in state, causing another render.
Handling Loading State
A real application should usually represent the loading state.
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then((response) => response.json())
.then((data) => {
setUsers(data);
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) {
return <p>Loading users...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Now the UI has a clear loading state.
Handling Fetch Errors
Network requests can fail, so applications should also handle errors.
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadUsers() {
try {
const response = await fetch("/api/users");
if (!response.ok) {
throw new Error("Failed to load users");
}
const data = await response.json();
setUsers(data);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
}
loadUsers();
}, []);
if (loading) {
return <p>Loading...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This gives the user three possible states:
Loading
↓
Success
or
Loading
↓
Error
For production applications, data-fetching libraries or framework-provided data-loading mechanisms may be preferable to manually managing every request with useEffect.
Fetching Data When a Dependency Changes
Suppose a page displays information for a particular user.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
async function loadUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
}
loadUser();
}, [userId]);
if (!user) {
return <p>Loading...</p>;
}
return <h1>{user.name}</h1>;
}
The dependency array contains:
[userId]
Therefore, when userId changes, the Effect runs again.
Avoiding Race Conditions in Data Fetching
When requests can overlap, an older request might finish after a newer request.
For example:
Request A → user 1
Request B → user 2
If Request B finishes first and Request A finishes later, Request A could incorrectly overwrite the state.
An AbortController can help cancel an outdated request:
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const data = await response.json();
setUser(data);
} catch (error) {
if (error.name !== "AbortError") {
setError(error.message);
}
}
}
loadUser();
return () => {
controller.abort();
};
}, [userId]);
When userId changes, the previous request can be aborted.
Using Timers with useEffect
Timers are another common Effect use case.
For example, you can create an interval:
useEffect(() => {
const intervalId = setInterval(() => {
console.log("Running...");
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
The cleanup function is important because the interval should stop when the component is no longer using it.
Creating a Countdown Timer
function Countdown() {
const [seconds, setSeconds] = useState(10);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((current) => {
if (current <= 1) {
clearInterval(intervalId);
return 0;
}
return current - 1;
});
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return <p>{seconds} seconds remaining</p>;
}
Notice the functional state update:
setSeconds((current) => current - 1);
This is useful because the next state depends on the previous state.
Using setTimeout
You can also use setTimeout.
useEffect(() => {
const timeoutId = setTimeout(() => {
console.log("Message displayed");
}, 2000);
return () => {
clearTimeout(timeoutId);
};
}, []);
If the component disappears before the timeout executes, the cleanup prevents the pending timeout from continuing.
Subscriptions
A subscription means that your component starts listening to an external source.
Examples include:
- WebSocket messages
- Chat updates
- Online status
- External stores
- Browser events
- Real-time notifications
A basic pattern looks like this:
useEffect(() => {
const unsubscribe = subscribeToUpdates((data) => {
console.log(data);
});
return () => {
unsubscribe();
};
}, []);
The subscription is created during setup.
The returned function removes the subscription.
WebSocket Example
A WebSocket connection is another example:
useEffect(() => {
const socket = new WebSocket("wss://example.com/socket");
socket.addEventListener("message", handleMessage);
return () => {
socket.removeEventListener("message", handleMessage);
socket.close();
};
}, []);
The cleanup closes the connection when it is no longer needed.
If the connection depends on a value such as a room ID:
useEffect(() => {
const socket = createRoomConnection(roomId);
socket.connect();
return () => {
socket.disconnect();
};
}, [roomId]);
Now the connection follows the current roomId.
Browser APIs
The browser provides many APIs that React does not control directly.
Examples include:
windowdocumentlocalStoragesessionStoragenavigatorIntersectionObserverResizeObserverGeolocationmatchMedia
Some of these APIs can be used directly in event handlers or during rendering when appropriate. Others require synchronization and are good candidates for Effects.
Browser Window Events
Suppose you want to monitor the browser width.
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <p>Window width: {width}px</p>;
}
The Effect establishes the relationship with the browser’s resize event.
The cleanup removes it.
In environments that render React on the server, accessing browser-only APIs such as window during initial rendering requires additional care because those APIs do not exist on the server.
Updating the Document Title
Another simple browser API example is document.title.
function ProductPage({ product }) {
useEffect(() => {
document.title = product.name;
}, [product.name]);
return <h1>{product.name}</h1>;
}
When the product name changes, the browser tab title is updated.
Using Local Storage
You can synchronize state with localStorage.
function ThemePreference() {
const [theme, setTheme] = useState("light");
useEffect(() => {
localStorage.setItem("theme", theme);
}, [theme]);
return (
<button onClick={() => setTheme("dark")}>
Use Dark Theme
</button>
);
}
Whenever theme changes, the Effect updates localStorage.
For reading initial values, however, you may not need an Effect. You can initialize state from storage when the environment supports it.
Avoiding Unnecessary Effects
One of the most important lessons about useEffect is:
Not every piece of code that runs after rendering needs an Effect.
Developers sometimes use useEffect for calculations that React can perform directly during rendering.
For example, this is unnecessary:
function ProductList({ products }) {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(products.length);
}, [products]);
return <p>Total products: {count}</p>;
}
The number of products can be calculated directly:
function ProductList({ products }) {
const count = products.length;
return <p>Total products: {count}</p>;
}
There is no need to store derived information as separate state.
Avoiding Duplicate State
Consider:
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
fullName is derived from firstName and lastName.
Instead:
const fullName = `${firstName} ${lastName}`;
This is simpler and avoids an unnecessary render cycle.
Calculate During Rendering When Possible
If a value can be calculated from existing props and state, calculate it during rendering.
For example:
function Cart({ items }) {
const total = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return <p>Total: ₹{total}</p>;
}
There is no reason to use:
useEffect(() => {
setTotal(...);
}, [items]);
The total is derived data.
Event Handlers vs Effects
A useful question is:
Is this operation caused by rendering, or by a specific user interaction?
If something happens because the user clicked a button, it often belongs in the event handler.
For example:
function SaveButton({ user }) {
function handleSave() {
saveUser(user);
}
return <button onClick={handleSave}>Save</button>;
}
There is no need to write:
useEffect(() => {
saveUser(user);
}, [user]);
That would cause saving to be tied to rendering rather than the user’s explicit action.
Effects Should Synchronize, Not Orchestrate Everything
A common mistake is using Effects to coordinate application logic.
For example:
useEffect(() => {
if (isLoggedIn) {
loadDashboard();
}
}, [isLoggedIn]);
Sometimes this is appropriate if the Effect is genuinely synchronizing with an external system.
But if loadDashboard() is directly caused by a button click, form submission, or navigation action, that operation may belong in the event handler or a dedicated data-loading mechanism instead.
Common useEffect Mistakes
useEffect is useful, but several mistakes appear frequently in React applications.
Mistake: Using Effects for Derived Values
Avoid:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Prefer:
const fullName = `${firstName} ${lastName}`;
Mistake: Missing Dependencies
Avoid:
useEffect(() => {
fetch(`/api/users/${userId}`);
}, []);
If the Effect needs to respond to changes in userId, include it:
useEffect(() => {
fetch(`/api/users/${userId}`);
}, [userId]);
Mistake: Incorrect Cleanup
Avoid creating a listener without removing it:
useEffect(() => {
window.addEventListener("resize", handleResize);
}, []);
Prefer:
useEffect(() => {
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
Mistake: Creating Timers Without Clearing Them
Avoid:
useEffect(() => {
setInterval(() => {
console.log("Running");
}, 1000);
}, []);
Prefer:
useEffect(() => {
const id = setInterval(() => {
console.log("Running");
}, 1000);
return () => {
clearInterval(id);
};
}, []);
Mistake: Updating State in an Effect Without a Good Reason
Consider:
useEffect(() => {
setCount(count + 1);
}, [count]);
This creates a loop because changing count causes another render, which runs the Effect again.
A component can quickly get stuck in repeated updates.
Effects that update state should have a clear reason and should not accidentally create render loops.
Mistake: Suppressing Dependency Warnings
If your Effect uses a reactive value, simply removing it from the dependency array is usually not the right solution.
For example:
useEffect(() => {
console.log(userId);
}, []);
Do not silence the warning without understanding why the dependency is intentionally excluded.
Instead, restructure the code or include the dependency when the Effect genuinely depends on it.
Mistake: Putting Event Logic in an Effect
Suppose a user clicks a button to purchase something.
Do not use an Effect merely to detect a state change:
useEffect(() => {
if (purchased) {
sendPurchaseRequest();
}
}, [purchased]);
If the purchase request is directly caused by the button click, it is usually clearer to perform the action in the event handler:
function handlePurchase() {
sendPurchaseRequest();
}
The Effect should be reserved for synchronization that follows from rendering or changing reactive values.
Mistake: Forgetting That Objects and Functions Have Identity
Consider:
const options = {
roomId,
serverUrl,
};
useEffect(() => {
connect(options);
}, [options]);
Because a new object is created during each render, options can have a new identity each time.
This can cause the Effect to run more often than expected.
One solution is to create the object inside the Effect:
useEffect(() => {
const options = {
roomId,
serverUrl,
};
connect(options);
}, [roomId, serverUrl]);
Similarly, functions created during rendering can have new identities.
Before adding useCallback simply to stabilize a function, first consider whether the Effect can be structured more simply.
A Practical Way to Think About Dependencies
When writing an Effect, ask:
What external system am I synchronizing with?
Then ask:
Which reactive values determine that synchronization?
For example:
useEffect(() => {
const connection = connectToRoom(roomId);
return () => {
connection.disconnect();
};
}, [roomId]);
The external system is the room connection.
The synchronization depends on:
roomId
Therefore:
[roomId]
is the dependency list.
A Complete Effect Example
Here is a more realistic example that combines fetching, loading, error handling, dependencies, and cleanup.
import { useEffect, useState } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function loadUser() {
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
if (!response.ok) {
throw new Error("Unable to load user");
}
const data = await response.json();
setUser(data);
} catch (error) {
if (error.name !== "AbortError") {
setError(error.message);
}
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
loadUser();
return () => {
controller.abort();
};
}, [userId]);
if (loading) {
return <p>Loading user...</p>;
}
if (error) {
return <p>Error: {error}</p>;
}
return <h1>{user?.name}</h1>;
}
This example demonstrates several important concepts:
useEffect- Dependency arrays
- Fetching data
- Loading state
- Error state
- Cleanup
AbortController- Synchronizing with changing
userId
A Simple Mental Model for Effects
You can think of an Effect as a synchronization process:
React renders component
↓
Effect setup runs
↓
External system is synchronized
↓
Dependencies change
↓
Old Effect cleanup
↓
New Effect setup
When the component is removed:
Component removed
↓
Final cleanup
This mental model is more useful than thinking of useEffect as simply “run this code after render.”
Effect Lifecycle Example
Consider:
useEffect(() => {
console.log("Connected");
return () => {
console.log("Disconnected");
};
}, [roomId]);
The lifecycle can look like:
Initial render
↓
Connected
roomId changes
↓
Disconnected
↓
Connected
Component unmounts
↓
Disconnected
This pattern is particularly useful for connections and subscriptions.
When Should You Use useEffect?
Use useEffect when your component needs to synchronize with an external system.
Common examples include:
| Situation | Effect? |
|---|---|
| Fetching external data | Often |
| WebSocket connection | Yes |
| Browser event listener | Yes |
| Timer | Yes |
| External subscription | Yes |
Updating document.title | Yes |
Calculating total from props | No |
| Formatting a string | No |
| Filtering an array for display | Usually no |
| Handling button clicks | Usually no |
| Form submission | Usually no |
| Deriving state from existing state | Usually no |
The key is not to memorize the table. Instead, ask whether you are synchronizing with something outside React.
Effects in a Web4Learners Tutorial Page
Imagine Web4Learners has a tutorial page with a selected lesson.
function TutorialPage({ lessonId }) {
const [lesson, setLesson] = useState(null);
useEffect(() => {
async function loadLesson() {
const response = await fetch(`/api/lessons/${lessonId}`);
const data = await response.json();
setLesson(data);
}
loadLesson();
}, [lessonId]);
return (
<main>
{lesson ? (
<>
<h1>{lesson.title}</h1>
<p>{lesson.description}</p>
</>
) : (
<p>Loading lesson...</p>
)}
</main>
);
}
Here, the Effect is appropriate because the component is synchronizing its UI with external lesson data.
When the user selects another lesson, lessonId changes and the Effect fetches the corresponding lesson.
Effects and Performance
Effects can indirectly affect application performance if they trigger unnecessary work.
For example:
useEffect(() => {
setFilteredProducts(
products.filter((product) => product.category === category)
);
}, [products, category]);
If filteredProducts is only derived data for rendering, this introduces unnecessary state and another render.
Instead:
const filteredProducts = products.filter(
(product) => product.category === category
);
If the calculation is genuinely expensive, React’s performance tools and memoization techniques can be considered separately. Do not automatically add useMemo or `useCallback just because an Effect runs.
Effects Are Not a Replacement for Event Handlers
This distinction is important.
An event handler answers:
What should happen because the user did something?
An Effect answers:
What should happen to synchronize this component with an external system?
For example:
function Newsletter() {
function handleSubscribe() {
subscribeUser();
}
return (
<button onClick={handleSubscribe}>
Subscribe
</button>
);
}
The subscription action is directly caused by the user’s click, so the event handler is the natural place for it.
An Effect would be appropriate if the component needed to synchronize an external subscription with a changing prop or state.
Effects and State Updates
Effects often update state after synchronizing with an external system.
For example:
useEffect(() => {
const handleOnline = () => {
setOnline(true);
};
const handleOffline = () => {
setOnline(false);
};
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
The browser event is external.
The React state represents that external information inside the component.
That is a good reason to use an Effect.
Best Practices for useEffect
Keep Effects focused on one synchronization concern whenever possible.
Identify the External System
Before writing an Effect, ask:
What am I connecting to or synchronizing with?
If the answer is “nothing,” you may not need an Effect.
Keep Dependencies Accurate
Do not intentionally omit dependencies simply to reduce Effect executions.
Instead, restructure the Effect when necessary.
Always Clean Up What You Create
If you create:
setInterval(...)
clean it up with:
clearInterval(...)
If you add:
addEventListener(...)
remove it with:
removeEventListener(...)
If you create a subscription, unsubscribe from it.
If you create a connection, disconnect it.
Avoid Duplicate State
If a value can be calculated from existing props or state, prefer deriving it during rendering.
Keep Event Logic in Event Handlers
Actions caused directly by user interaction usually belong in event handlers.
Handle Async Operations Carefully
When using Effects for requests, consider:
- Loading states
- Error states
- Cancellation
- Changing dependencies
- Race conditions
- Stale results
Test Effects With Strict Mode
If your Effect produces unexpected behavior during development, check whether setup and cleanup are correctly symmetrical.
Effect Hooks Cheat Sheet
| Concept | Purpose |
|---|---|
useEffect | Synchronize with external systems |
| Setup function | Starts the synchronization |
| Cleanup function | Stops or reverses the synchronization |
| Dependency array | Controls when synchronization is re-established |
[] | Effect has no reactive dependencies |
[value] | Re-synchronize when value changes |
| No dependency array | Run after every completed render |
| Fetching | Can synchronize component with external data |
| Timer | Start timer and clean it up |
| Subscription | Subscribe and unsubscribe |
| Browser API | Synchronize with browser-managed systems |
| Derived value | Usually calculate during rendering instead |
| User action | Usually handle with event handlers |
Practice Exercise
Create a small React application called Web4Learners Online Status.
The application should:
- Display whether the browser is online
- Listen for
onlineandofflineevents - Store the current status in state
- Update the UI when the browser connection changes
- Remove the event listeners during cleanup
A possible starting point is:
import { useEffect, useState } from "react";
function OnlineStatus() {
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 (
<p>
Status: {isOnline ? "Online" : "Offline"}
</p>
);
}
export default OnlineStatus;
This exercise demonstrates the complete Effect pattern:
External browser events
↓
useEffect
↓
Event listeners
↓
React state
↓
UI update
↓
Cleanup
Key Takeaways
- An Effect allows a React component to synchronize with an external system.
useEffectis the Hook used to create Effects.- Effects run after React commits the component’s rendered output.
- The dependency array describes the reactive values that the Effect depends on.
- An empty dependency array means the Effect has no reactive dependencies.
- Without a dependency array, an Effect runs after every completed render.
- Cleanup functions undo or disconnect resources created by an Effect.
- Timers should be cleared during cleanup.
- Event listeners should be removed during cleanup.
- Subscriptions should be unsubscribed.
- Connections should be disconnected.
- Data fetching can be performed with Effects, but larger applications may benefit from dedicated data-loading solutions.
- Effects can depend on props and state.
- Do not use Effects simply to calculate derived values.
- Avoid storing duplicate or derived state when it can be calculated during rendering.
- User interactions generally belong in event handlers.
- Missing dependencies can cause Effects to use outdated values.
- Incorrect dependencies can cause unnecessary Effect executions.
- Effects that update state can accidentally create infinite render loops.
- Objects and functions created during rendering can affect dependency identity.
- A good Effect has a clear synchronization purpose.
- Setup and cleanup should work together as a balanced pair.
The most important idea to remember is simple:
Use Effects to synchronize React with the world outside React, not as a general-purpose place to run code after rendering.