Introduction to React useEffect
As your React applications become more interactive, you'll often need to perform tasks that go beyond simply rendering UI.
For example, you might need to:
- Fetch data from an API
- Update the page title
- Save user preferences
- Listen for browser events
- Start a timer
- Connect to a WebSocket
- Store data in Local Storage
These operations are known as side effects, and React provides the useEffect Hook to handle them.
If you've already learned useState, Forms, and Event Handling, then useEffect is the next essential hook to master.
In this guide, you'll learn how useEffect works, when to use it, and how to avoid common mistakes using practical, real-world examples.
What You'll Learn
By the end of this tutorial, you'll be able to:
- Understand what side effects are
- Use the
useEffectHook correctly - Run code after rendering
- Work with dependency arrays
- Fetch data from APIs
- Create timers
- Save data to Local Storage
- Add and remove event listeners
- Clean up resources properly
- Avoid infinite loops
- Follow React best practices
Let's begin by understanding what side effects actually are.
What Are Side Effects?
A side effect is any operation that interacts with something outside of the React rendering process.
Examples include:
- Calling an API
- Updating the browser title
- Reading Local Storage
- Writing Local Storage
- Starting a timer
- Listening for window resize events
- Opening a WebSocket connection
These tasks don't directly generate the user interface, but they are necessary for many real-world applications.
For example:
User Opens Product Page
│
▼
React Renders Component
│
▼
Fetch Product Details
│
▼
Receive API Response
│
▼
Update State
│
▼
Display Product
The API request is a side effect because it happens outside of rendering the component.
Why Do We Need useEffect?
React components should remain focused on rendering the UI.
Imagine this component:
jsxfunction App() { document.title = "Welcome"; return <h1>Hello React</h1>; }
Although this works, React recommends moving side effects into useEffect.
jsximport { useEffect } from "react"; function App() { useEffect(() => { document.title = "Welcome"; }); return <h1>Hello React</h1>; }
Now React knows that updating the document title is a side effect.
This makes components easier to understand and maintain.
Understanding Component Lifecycle
Before Hooks were introduced, React developers used lifecycle methods in class components.
A component goes through three main phases:
Component Created
│
▼
Mounted
│
▼
Updated
│
▼
Unmounted
Mount
The component appears on the screen for the first time.
Examples:
- Home page loads
- Product page opens
- Dashboard appears
Update
The component re-renders because:
- State changed
- Props changed
- Parent component re-rendered
Unmount
The component is removed from the page.
Examples:
- User leaves the page
- Modal closes
- Component is hidden
How useEffect Replaces Lifecycle Methods
In older React class components, developers used:
componentDidMount()componentDidUpdate()componentWillUnmount()
With Hooks, all of these behaviors can be handled using useEffect.
Instead of learning multiple lifecycle methods, you only need to understand one Hook.
This simplifies React development while keeping your code more organized.
Basic Syntax of useEffect
jsximport { useEffect } from "react"; function App() { useEffect(() => { console.log("Effect executed."); }); return <h1>Hello React</h1>; }
The syntax has two parts:
jsxuseEffect(() => { // Side effect code });
The callback function contains the code you want React to execute after rendering the component.
When Does useEffect Run?
By default, useEffect runs after every render.
Example:
jsximport { useEffect, useState } from "react"; function Counter() { const [count, setCount] = useState(0); useEffect(() => { console.log("Component rendered."); }); return ( <button onClick={() => setCount(count + 1)} > Count: {count} </button> ); }
Every time you click the button:
Button Click
│
▼
State Updates
│
▼
Component Re-renders
│
▼
useEffect Executes
If you click five times, the message appears five times in the console.
This behavior is important to understand because many beginners accidentally trigger unnecessary effects by forgetting to control when useEffect should run.
In the next section, you'll learn how dependency arrays allow you to control exactly when an effect executes.
Understanding Dependency Arrays
One of the most important parts of the useEffect Hook is the dependency array.
The dependency array tells React when an effect should run.
jsxuseEffect(() => { // Side effect }, []);
The array is always the second argument passed to useEffect.
Depending on what you put inside this array, React behaves differently.
There are three common scenarios:
- No dependency array
- Empty dependency array
- Dependency array with values
Let's understand each one.
1. No Dependency Array
If you don't provide a dependency array, the effect runs after every render.
jsximport { useEffect, useState } from "react"; export default function App() { const [count, setCount] = useState(0); useEffect(() => { console.log("Effect executed"); }); return ( <button onClick={() => setCount(count + 1)}> {count} </button> ); }
Console Output
Effect executed
Effect executed
Effect executed
Effect executed
...
Every state update causes:
State Changes
│
▼
Component Re-renders
│
▼
useEffect Executes
This is rarely what you want.
2. Empty Dependency Array
Adding an empty dependency array tells React to execute the effect only once.
jsxuseEffect(() => { console.log("Component Mounted"); }, []);
This is similar to the old
componentDidMount()
in class components.
Execution Flow
Page Loads
│
▼
Component Mounts
│
▼
useEffect Runs Once
Even if state changes later,
jsxsetCount(count + 1);
the effect will not run again.
This is commonly used for:
- API Calls
- Initial Data Loading
- Authentication Check
- Local Storage Reading
- Initial Configuration
Example
jsximport { useEffect } from "react"; export default function Home() { useEffect(() => { console.log("Welcome"); }, []); return <h1>Home</h1>; }
Output
Welcome
Only once.
3. Dependency Array with Variables
Sometimes you want the effect to run only when a specific value changes.
Example:
jsximport { useState, useEffect } from "react"; export default function App() { const [count, setCount] = useState(0); useEffect(() => { console.log("Count changed"); }, [count]); return ( <button onClick={() => setCount(count + 1)}> {count} </button> ); }
Now React watches only
count
Whenever it changes,
Count Updated
│
▼
useEffect Runs
Multiple Dependencies
You can also watch multiple values.
jsxconst [name, setName] = useState(""); const [age, setAge] = useState(18); useEffect(() => { console.log("Data Changed"); }, [name, age]);
Now the effect runs whenever
- name changes
- age changes
Flow
Name Changed
│
├──────────┐
▼ │
useEffect Runs │
│
Age Changed │
│ │
└──────────┘
Real Example
Imagine a shopping website.
jsxconst [category, setCategory] = useState("Mobiles"); useEffect(() => { fetchProducts(category); }, [category]);
Whenever the user selects another category,
Mobiles
↓
Laptops
↓
Gaming
↓
Accessories
React automatically fetches new products.
No manual refresh required.
Dependency Arrays and Objects
One mistake beginners often make is using objects directly inside dependencies.
jsxconst user = { name: "John" }; useEffect(() => { console.log(user); }, [user]);
Since a new object is created on every render, React thinks the dependency has changed and runs the effect again.
This can lead to unnecessary renders or repeated API calls.
If you need stable object references, consider using useMemo or restructuring your code.
Dependency Arrays and Functions
The same issue applies to functions.
jsxfunction fetchUsers() { console.log("Fetching..."); } useEffect(() => { fetchUsers(); }, [fetchUsers]);
Since functions are recreated on every render, the effect may run more often than expected.
In larger applications, developers often use useCallback to create stable function references.
We'll cover useCallback in a dedicated tutorial later in this React series.
Summary
The dependency array determines when your effect executes.
| Dependency Array | Runs When |
|---|---|
| No array | After every render |
[] | Only once after the component mounts |
[count] | When count changes |
[name, age] | When name or age changes |
Understanding dependency arrays is the key to using useEffect effectively.
In the next section, we'll explore cleanup functions, where you'll learn how to stop timers, remove event listeners, and prevent memory leaks when components are removed from the page.
Cleanup Functions in useEffect
So far, we've learned how to execute code using useEffect.
However, some side effects continue running even after a component disappears from the screen.
Examples include:
- Timers
- Event listeners
- WebSocket connections
- API subscriptions
- Intervals
If these aren't cleaned up properly, they can cause:
- Memory leaks
- Unexpected behavior
- Duplicate event listeners
- Poor performance
React solves this problem using cleanup functions.
What is a Cleanup Function?
A cleanup function is a function returned from inside useEffect.
jsxuseEffect(() => { // Effect return () => { // Cleanup }; }, []);
When React removes the component from the page, it automatically executes the cleanup function.
Think of it like this:
Component Mounts
│
▼
Run Effect
│
▼
Component Active
│
▼
Component Unmounts
│
▼
Cleanup Function Executes
Why Cleanup is Important
Imagine starting a timer.
jsxuseEffect(() => { setInterval(() => { console.log("Running"); }, 1000); }, []);
Now suppose the user leaves the page.
Without cleanup:
User Leaves Page
│
▼
Timer Keeps Running ❌
Every time the user returns to the page, another timer starts.
Eventually you may have:
Timer 1
Timer 2
Timer 3
Timer 4
all running together.
This wastes memory and slows down the application.
Cleaning Up Timers
The correct approach is:
jsximport { useEffect } from "react"; export default function Timer() { useEffect(() => { const timer = setInterval(() => { console.log("Running..."); }, 1000); return () => { clearInterval(timer); }; }, []); return <h1>Timer Example</h1>; }
Now the sequence becomes:
Component Mounts
│
▼
Start Timer
│
▼
User Leaves Page
│
▼
clearInterval()
│
▼
Timer Stops ✅
Cleanup with setTimeout()
The same principle applies to setTimeout().
Incorrect:
jsxuseEffect(() => { setTimeout(() => { console.log("Hello"); }, 5000); }, []);
Correct:
jsxuseEffect(() => { const timeout = setTimeout(() => { console.log("Hello"); }, 5000); return () => { clearTimeout(timeout); }; }, []);
Removing Event Listeners
Suppose you want to detect browser resizing.
jsxuseEffect(() => { function handleResize() { console.log(window.innerWidth); } window.addEventListener( "resize", handleResize ); }, []);
Looks fine...
Until the component mounts again.
Now another listener is added.
Resize
↓
Listener 1
↓
Listener 2
↓
Listener 3
Eventually every resize executes multiple listeners.
Correct Version
jsxuseEffect(() => { function handleResize() { console.log(window.innerWidth); } window.addEventListener( "resize", handleResize ); return () => { window.removeEventListener( "resize", handleResize ); }; }, []);
Now React removes the listener automatically.
Real Example: Window Width Tracker
jsximport { useEffect, useState } from "react"; export default function WindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { function updateWidth() { setWidth(window.innerWidth); } window.addEventListener( "resize", updateWidth ); return () => { window.removeEventListener( "resize", updateWidth ); }; }, []); return <h2>Width: {width}px</h2>; }
Now the width updates whenever the browser is resized, and React removes the listener when the component unmounts.
Cleaning Up API Subscriptions
Some APIs maintain long-lived connections.
Examples include:
- WebSocket
- Firebase
- Supabase Realtime
- Socket.IO
- Chat applications
Example:
jsxuseEffect(() => { const socket = connect(); return () => { socket.disconnect(); }; }, []);
When the user leaves the page:
Disconnect Socket
↓
Release Memory
↓
Close Connection
Without cleanup, the connection would remain open.
When Does Cleanup Run?
A common misconception is that cleanup only runs when a component unmounts.
Actually, cleanup runs in two situations:
1. Before the effect runs again
If an effect depends on a value:
jsxuseEffect(() => { console.log("Effect"); return () => { console.log("Cleanup"); }; }, [count]);
Every time count changes:
Cleanup Previous Effect
↓
Run New Effect
This ensures React doesn't leave outdated effects running.
2. When the component unmounts
If the component is removed from the page:
Component Removed
↓
Cleanup Executes
↓
Resources Released
Common Mistakes
Forgetting Cleanup
❌
jsxwindow.addEventListener( "resize", handleResize );
Every render adds another listener.
Correct
✅
jsxreturn () => { window.removeEventListener( "resize", handleResize ); };
Forgetting clearInterval()
❌
jsxsetInterval(...);
The timer continues forever.
Correct
✅
jsxconst timer = setInterval(...); return () => { clearInterval(timer); };
Best Practices
- Always clean up timers.
- Remove event listeners when they're no longer needed.
- Disconnect WebSockets and subscriptions.
- Avoid creating duplicate listeners.
- Remember that cleanup also runs before an effect re-executes because its dependencies changed.
Following these practices helps prevent memory leaks and keeps your React applications fast and reliable.
Summary
Cleanup functions are an essential part of useEffect. They allow you to stop timers, remove event listeners, close connections, and release resources when they're no longer needed.
Without proper cleanup, your application can accumulate unused timers, duplicate listeners, and open connections, leading to bugs and performance issues.
In the next section, we'll explore one of the most common uses of useEffect: fetching data from APIs, including loading states, error handling, and displaying data in your React components.
Fetching Data from APIs with useEffect
One of the most common uses of the useEffect Hook is fetching data from an API.
Modern web applications rarely contain all of their data inside the frontend. Instead, they retrieve information from backend services.
Some examples include:
- Loading products from an e-commerce API
- Fetching blog posts
- Displaying user profiles
- Showing weather information
- Loading order history
- Retrieving dashboard statistics
The typical flow looks like this:
Component Loads
│
▼
useEffect Executes
│
▼
API Request Sent
│
▼
Server Responds
│
▼
Update State
│
▼
React Re-renders UI
Basic API Example
Let's start with a simple example using the Fetch API.
jsximport { useEffect, useState } from "react"; export default function Users() { const [users, setUsers] = useState([]); useEffect(() => { fetch("https://jsonplaceholder.typicode.com/users") .then((response) => response.json()) .then((data) => { setUsers(data); }); }, []); return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} </li> ))} </ul> ); }
Since the dependency array is empty ([]), the API request is made only once when the component mounts.
Why Use an Empty Dependency Array?
Imagine if you removed the dependency array.
jsxuseEffect(() => { fetchUsers(); });
Now every render triggers another API call.
Render
↓
API Request
↓
State Updates
↓
Render Again
↓
API Request Again
↓
Render Again
↓
API Request Again
...
This creates an infinite loop of requests.
Using an empty dependency array prevents that behavior.
jsxuseEffect(() => { fetchUsers(); }, []);
Now the request runs only once.
Using async/await
Many developers prefer async/await because it is easier to read.
jsxuseEffect(() => { async function fetchUsers() { const response = await fetch( "https://jsonplaceholder.typicode.com/users" ); const data = await response.json(); setUsers(data); } fetchUsers(); }, []);
Notice that the async function is declared inside useEffect.
You shouldn't make the effect callback itself async.
❌ Incorrect
jsxuseEffect(async () => { }, []);
React expects the callback to either return nothing or return a cleanup function, not a Promise.
Showing a Loading State
API requests are not instant.
Users should know that data is loading.
jsxconst [loading, setLoading] = useState(true);
Inside the request:
jsxasync function fetchUsers() { const response = await fetch(...); const data = await response.json(); setUsers(data); setLoading(false); }
Display the loading message.
jsxif (loading) { return <h2>Loading...</h2>; }
Flow:
Page Opens
↓
Loading...
↓
API Response
↓
Display Users
A loading indicator greatly improves the user experience.
Handling API Errors
Sometimes requests fail because of:
- Network issues
- Server errors
- Invalid endpoints
- Authentication failures
Always handle errors gracefully.
jsxconst [error, setError] = useState("");
Example:
jsxasync function fetchUsers() { try { const response = await fetch( "https://jsonplaceholder.typicode.com/users" ); const data = await response.json(); setUsers(data); } catch (err) { setError( "Unable to load users." ); } finally { setLoading(false); } }
Display the error.
jsxif (error) { return <h2>{error}</h2>; }
Users receive helpful feedback instead of seeing a blank page.
Complete Example
jsximport { useEffect, useState } from "react"; export default function Users() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); useEffect(() => { async function fetchUsers() { try { const response = await fetch( "https://jsonplaceholder.typicode.com/users" ); if (!response.ok) { throw new Error( "Request failed." ); } const data = await response.json(); setUsers(data); } catch (err) { setError( "Failed to fetch users." ); } finally { setLoading(false); } } fetchUsers(); }, []); if (loading) { return <h2>Loading...</h2>; } if (error) { return <h2>{error}</h2>; } return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} </li> ))} </ul> ); }
Fetching Data When a Dependency Changes
Sometimes data should reload whenever a value changes.
Imagine an online store where users choose a product category.
jsxconst [category, setCategory] = useState("electronics");
Now fetch products whenever the category changes.
jsxuseEffect(() => { async function fetchProducts() { const response = await fetch( `/api/products?category=${category}` ); const data = await response.json(); setProducts(data); } fetchProducts(); }, [category]);
Flow:
Electronics
↓
Books
↓
Fashion
↓
Accessories
Each time the category changes, React automatically fetches fresh data.
Best Practices for API Calls
When fetching data with useEffect, keep these tips in mind:
- Use an empty dependency array for initial data loading.
- Display a loading state while waiting for the response.
- Handle errors gracefully.
- Avoid making the effect callback
async. - Validate the server response before using the data.
- Keep API logic inside a separate function for better readability.
- Consider moving reusable API logic into a custom Hook as your application grows.
Summary
Fetching data from APIs is one of the most common reasons to use the useEffect Hook. By combining useEffect with state management, loading indicators, and proper error handling, you can build responsive and user-friendly applications.
In the next section, we'll explore more practical examples of useEffect, including Local Storage, Dark Mode, Timers, and Window Event Listeners, which are frequently used in real-world React projects.
Real-World useEffect Examples
So far, you've learned the fundamentals of useEffect, dependency arrays, cleanup functions, and API calls.
Now let's explore some practical examples that you'll frequently encounter in real React applications.
These examples demonstrate how useEffect interacts with the browser and external resources.
Example 1: Updating the Page Title
A common use case is changing the browser tab title dynamically.
jsximport { useState, useEffect } from "react"; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Clicked ${count} times`; }, [count]); return ( <button onClick={() => setCount(count + 1)} > Clicked {count} </button> ); }
Every click updates:
Clicked 0 times
↓
Clicked 1 times
↓
Clicked 2 times
This technique is useful for:
- Notifications
- Shopping cart item count
- Dashboard updates
- Active chat messages
Example 2: Saving Data to Local Storage
Users expect applications to remember their preferences.
For example:
- Dark Mode
- Language
- Theme
- Username
- Shopping Cart
React can automatically save values whenever they change.
jsximport { useState, useEffect } from "react"; export default function App() { const [username, setUsername] = useState(""); useEffect(() => { localStorage.setItem( "username", username ); }, [username]); return ( <input value={username} onChange={(e) => setUsername(e.target.value) } /> ); }
Flow
User Types
↓
State Updates
↓
useEffect Runs
↓
Save to Local Storage
Reading from Local Storage
Saving data isn't enough.
When the application loads, we also want to restore it.
jsxuseEffect(() => { const savedName = localStorage.getItem("username"); if (savedName) { setUsername(savedName); } }, []);
Now users won't lose their data after refreshing the page.
Example 3: Dark Mode
Many modern applications allow users to switch between light and dark themes.
jsxconst [darkMode, setDarkMode] = useState(false); useEffect(() => { document.body.classList.toggle( "dark", darkMode ); }, [darkMode]);
Whenever the user changes the theme:
Light
↓
Dark
↓
Light
React automatically updates the page.
Example 4: Auto Save
Imagine writing a blog post.
Instead of waiting until the user clicks Save, React can automatically save changes.
jsxconst [content, setContent] = useState(""); useEffect(() => { localStorage.setItem( "draft", content ); }, [content]);
Every keystroke updates the saved draft.
Applications like:
- Google Docs
- Notion
- Medium
use similar techniques.
Example 5: Digital Clock
A digital clock updates every second.
jsximport { useEffect, useState } from "react"; export default function Clock() { const [time, setTime] = useState(new Date()); useEffect(() => { const timer = setInterval(() => { setTime(new Date()); }, 1000); return () => { clearInterval(timer); }; }, []); return ( <h2> {time.toLocaleTimeString()} </h2> ); }
This combines:
- State
- useEffect
- Cleanup Function
- Timer
Example 6: Countdown Timer
jsxconst [seconds, setSeconds] = useState(10); useEffect(() => { if (seconds === 0) return; const timer = setTimeout(() => { setSeconds(seconds - 1); }, 1000); return () => { clearTimeout(timer); }; }, [seconds]);
Output
10
↓
9
↓
8
↓
...
↓
0
Perfect for:
- OTP verification
- Quiz timers
- Flash sales
- Online exams
Example 7: Scroll Position Tracker
Sometimes applications need to know how far a user has scrolled.
jsximport { useEffect, useState } from "react"; export default function ScrollTracker() { const [scrollY, setScrollY] = useState(0); useEffect(() => { function handleScroll() { setScrollY(window.scrollY); } window.addEventListener( "scroll", handleScroll ); return () => { window.removeEventListener( "scroll", handleScroll ); }; }, []); return <h2>{scrollY}px</h2>; }
Applications use this for:
- Sticky navigation bars
- Reading progress indicators
- Infinite scrolling
- Back-to-top buttons
Example 8: Online / Offline Status
You can even detect whether a user is connected to the internet.
jsximport { useEffect, useState } from "react"; export default function NetworkStatus() { const [online, setOnline] = useState(navigator.onLine); useEffect(() => { function goOnline() { setOnline(true); } function goOffline() { setOnline(false); } window.addEventListener( "online", goOnline ); window.addEventListener( "offline", goOffline ); return () => { window.removeEventListener( "online", goOnline ); window.removeEventListener( "offline", goOffline ); }; }, []); return ( <h2> {online ? "Online ✅" : "Offline ❌"} </h2> ); }
This is especially useful for:
- Chat applications
- Banking apps
- E-commerce websites
- Offline-first applications
Summary
These examples demonstrate how versatile the useEffect Hook is. Whether you're updating the page title, saving user preferences, managing timers, tracking browser events, or responding to network changes, useEffect provides a consistent way to synchronize your React components with the outside world.
As you build more complex applications, you'll find yourself using these patterns repeatedly.
In the next section, we'll look at the most common mistakes developers make with useEffect, explain why they happen, and show you how to avoid them.
Common Mistakes with useEffect
The useEffect Hook is powerful, but it's also one of the most misunderstood parts of React.
Many bugs such as:
- Infinite API requests
- Slow performance
- Memory leaks
- Unexpected renders
- Missing updates
are caused by using useEffect incorrectly.
Let's look at the most common mistakes and how to avoid them.
Mistake 1: Forgetting the Dependency Array
One of the most common beginner mistakes is omitting the dependency array.
❌ Incorrect
jsxuseEffect(() => { fetchProducts(); });
This runs after every render.
Flow:
Render
↓
API Call
↓
State Updates
↓
Render
↓
API Call
↓
Render
↓
API Call
Eventually your application continuously sends requests.
Correct
jsxuseEffect(() => { fetchProducts(); }, []);
Now the API is called only once.
Mistake 2: Creating Infinite Loops
Another common mistake is updating the same state that the effect depends on.
❌
jsxconst [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); }, [count]);
Execution
Count Changes
↓
Effect Runs
↓
Count Changes
↓
Effect Runs
↓
Forever...
React keeps rendering until it throws:
Maximum update depth exceeded
Correct
Only update state when necessary.
jsxuseEffect(() => { console.log(count); }, [count]);
Or move the state update to an event handler.
Mistake 3: Missing Dependencies
Sometimes developers intentionally leave dependencies out.
jsxuseEffect(() => { console.log(name); }, []);
If name changes later,
the effect never runs again.
React even warns you:
React Hook useEffect has a missing dependency.
Correct
jsxuseEffect(() => { console.log(name); }, [name]);
Now the effect always uses the latest value.
Mistake 4: Ignoring ESLint Warnings
Many developers see this warning:
React Hook has missing dependencies.
Instead of fixing it,
they disable ESLint.
❌
jsx// eslint-disable-next-line
This hides the warning but doesn't solve the problem.
Always understand why React suggests adding a dependency before suppressing the warning.
Mistake 5: Making useEffect async
This is another common error.
❌
jsxuseEffect(async () => { const response = await fetch("/api"); }, []);
React expects the callback to return either:
- nothing
- a cleanup function
An async function returns a Promise instead.
Correct
jsxuseEffect(() => { async function loadData() { const response = await fetch("/api"); const data = await response.json(); } loadData(); }, []);
Mistake 6: Forgetting Cleanup
Consider this code.
jsxuseEffect(() => { window.addEventListener( "resize", handleResize ); }, []);
Every time the component mounts,
another listener is added.
Eventually:
Resize
↓
Listener 1
↓
Listener 2
↓
Listener 3
Multiple listeners execute simultaneously.
Correct
jsxuseEffect(() => { window.addEventListener( "resize", handleResize ); return () => { window.removeEventListener( "resize", handleResize ); }; }, []);
Mistake 7: Using Objects as Dependencies
Consider this example.
jsxconst user = { name: "John" }; useEffect(() => { console.log(user); }, [user]);
Looks fine...
But React creates a new object every render.
Render
↓
New Object
↓
Dependency Changed
↓
Effect Runs
Even if nothing actually changed.
Better Approach
Store objects in state or memoize them.
jsxconst user = useMemo(() => ({ name: "John" }), []);
This keeps the object reference stable.
Mistake 8: Using Functions as Dependencies
Functions behave similarly.
jsxfunction loadUsers() { }
Every render creates a new function.
React sees:
New Function
↓
Dependency Changed
↓
Run Effect
Better Solution
Use useCallback.
jsxconst loadUsers = useCallback(() => { // Fetch users }, []);
We'll learn useCallback in a later article.
Mistake 9: Putting Everything Inside One Effect
Some beginners write enormous effects.
jsxuseEffect(() => { fetchProducts(); document.title = "Shop"; window.addEventListener(...); localStorage.setItem(...); }, []);
This becomes difficult to read and maintain.
Better Practice
Split unrelated logic.
jsxuseEffect(() => { fetchProducts(); }, []); useEffect(() => { document.title = "Shop"; }, []); useEffect(() => { localStorage.setItem( "theme", theme ); }, [theme]);
Each effect has one responsibility.
Mistake 10: Using useEffect When You Don't Need It
Not every piece of logic belongs in useEffect.
For example:
❌
jsxconst fullName = firstName + " " + lastName; useEffect(() => { setName(fullName); }, [firstName, lastName]);
This creates unnecessary state and an extra render.
Better
Simply calculate the value during rendering.
jsxconst fullName = `${firstName} ${lastName}`;
No useEffect required.
A good rule is:
If a value can be calculated from existing props or state during rendering, you usually don't need an effect.
Best Practices
Follow these recommendations whenever you write an effect:
- Keep each effect focused on a single task.
- Always specify dependencies correctly.
- Never ignore ESLint warnings without understanding them.
- Clean up timers, event listeners, and subscriptions.
- Avoid storing derived values in state.
- Use
useMemoanduseCallbackonly when they solve a real problem. - Use
useEffectfor synchronizing with external systems, not for ordinary calculations.
Quick Checklist
Before writing a useEffect, ask yourself:
✅ Does this interact with something outside React?
✅ Does it fetch data?
✅ Does it subscribe to an event?
✅ Does it start a timer?
✅ Does it update the browser?
If the answer is yes, useEffect is probably appropriate.
If the logic simply derives one value from another, you likely don't need an effect.
Summary
Understanding these common mistakes will help you write cleaner and more efficient React applications. Most issues with useEffect come from incorrect dependencies, missing cleanup, or using it for work that doesn't belong in an effect.
By keeping your effects focused, cleaning up resources, and respecting dependency arrays, you'll avoid many of the bugs that frustrate new React developers.
In the next section, we'll wrap up this guide with Frequently Asked Questions (FAQs), Interview Questions, a quick recap, and key takeaways to reinforce everything you've learned.
Frequently Asked Questions (FAQs)
1. What is the useEffect Hook?
The useEffect Hook allows you to perform side effects in React components.
Common side effects include:
- Fetching data from an API
- Updating the browser title
- Reading or writing Local Storage
- Starting timers
- Adding event listeners
- Connecting to WebSockets
2. When does useEffect run?
It depends on the dependency array.
jsxuseEffect(() => { console.log("Runs after every render"); });
Runs after every render.
jsxuseEffect(() => { console.log("Runs once"); }, []);
Runs only once after the component mounts.
jsxuseEffect(() => { console.log("Runs when count changes"); }, [count]);
Runs whenever count changes.
3. Why do we use the dependency array?
The dependency array tells React when an effect should execute.
Without it:
Every Render
With it:
Only when dependencies change
This improves performance and prevents unnecessary work.
4. Can I have multiple useEffect Hooks?
Yes.
In fact, it's recommended.
Instead of writing one large effect,
write several smaller ones.
Example:
jsxuseEffect(() => { fetchProducts(); }, []); useEffect(() => { document.title = title; }, [title]); useEffect(() => { localStorage.setItem( "theme", theme ); }, [theme]);
Each effect has a single responsibility.
5. Can I use async directly in useEffect?
No.
Incorrect:
jsxuseEffect(async () => { }, []);
Correct:
jsxuseEffect(() => { async function loadData() { } loadData(); }, []);
6. Why does React ask for cleanup functions?
Cleanup functions help React remove resources that are no longer needed.
Examples include:
- Timers
- Event listeners
- WebSockets
- Subscriptions
Without cleanup, your application may suffer from memory leaks or duplicate listeners.
7. Does useEffect replace lifecycle methods?
Yes.
In class components, developers used:
- componentDidMount()
- componentDidUpdate()
- componentWillUnmount()
With Hooks, useEffect covers these scenarios depending on how you configure the dependency array and cleanup function.
8. Can I fetch APIs using useEffect?
Absolutely.
This is one of its most common use cases.
jsxuseEffect(() => { async function fetchUsers() { const response = await fetch("/api/users"); const data = await response.json(); setUsers(data); } fetchUsers(); }, []);
9. What happens if I forget the dependency array?
The effect runs after every render.
This may result in:
- Repeated API requests
- Performance issues
- Infinite loops
- Unnecessary computations
Always think carefully about when an effect should run.
10. When should I avoid useEffect?
Avoid using useEffect for values that can be computed directly during rendering.
For example:
jsxconst fullName = `${firstName} ${lastName}`;
There's no need to store fullName in state or update it with an effect.
React useEffect Interview Questions
Here are some common interview questions to test your understanding of the useEffect Hook.
Beginner Level
1. What is the purpose of the useEffect Hook?
Answer:
It allows React components to perform side effects such as API calls, timers, event listeners, and updating browser APIs after rendering.
2. What are side effects?
Answer:
Side effects are operations that interact with systems outside React's rendering process, such as fetching data, accessing Local Storage, or subscribing to browser events.
3. What is the dependency array?
Answer:
The dependency array tells React when an effect should run.
Examples:
- No array → after every render
[]→ once after mount[count]→ whenevercountchanges
4. Why shouldn't you make the useEffect callback async?
Answer:
An async function returns a Promise, but React expects the callback to return either nothing or a cleanup function.
Instead, define an async function inside the effect and call it.
5. Why are cleanup functions important?
Answer:
Cleanup functions prevent memory leaks by removing timers, event listeners, subscriptions, and other resources when they're no longer needed.
Intermediate Level
6. What causes an infinite loop in useEffect?
Answer:
Updating state inside an effect while also depending on that same state.
Example:
jsxuseEffect(() => { setCount(count + 1); }, [count]);
7. What happens if you omit a dependency?
Answer:
The effect may use outdated values because it won't re-run when that dependency changes. React's Hooks ESLint plugin will typically warn about this.
8. Why should unrelated logic be split into multiple useEffect Hooks?
Answer:
Smaller effects are easier to read, test, maintain, and reason about. Each effect should ideally synchronize one concern.
9. When does the cleanup function execute?
Answer:
A cleanup function runs:
- Before the effect re-runs due to dependency changes.
- When the component unmounts.
10. When should you use useEffect?
Answer:
Use useEffect whenever your component needs to synchronize with something outside React, such as:
- APIs
- Browser APIs
- Timers
- Event listeners
- Local Storage
- WebSocket connections
Summary
Congratulations! 🎉
You've now learned one of the most important Hooks in React.
Throughout this guide, you explored:
- What side effects are
- Why
useEffectexists - Component lifecycle concepts
- Dependency arrays
- Cleanup functions
- Fetching API data
- Local Storage
- Timers
- Event listeners
- Real-world examples
- Common mistakes
- Best practices
- Frequently asked questions
- Interview questions
The useEffect Hook helps your React components stay synchronized with the outside world while keeping rendering predictable and efficient.
As you build larger applications, you'll use useEffect for tasks such as authentication, API integration, analytics, notifications, and browser interactions. Understanding when—and just as importantly, when not—to use it is a key step toward writing clean React applications.
Key Takeaways
useEffectis used for side effects.- The dependency array controls when an effect runs.
- Use
[]to run an effect only once after the component mounts. - Always clean up timers, subscriptions, and event listeners.
- Avoid updating state in a way that creates infinite loops.
- Don't make the effect callback
async; create an async function inside it instead. - Split unrelated logic into multiple effects.
- Don't use
useEffectfor values that can be derived during rendering.
What's Next?
Now that you've mastered useEffect, it's time to learn how to share state across multiple components without prop drilling.
In the next tutorial, we'll cover React Context API Explained, where you'll learn:
- What the Context API is
- Why prop drilling is a problem
- Creating and providing context
- Consuming context with
useContext - Managing global themes and user authentication
- Best practices and common pitfalls
- Real-world examples and interview questions
By the end of that guide, you'll be able to build cleaner, more scalable React applications with shared state.



