Every application we build will eventually need to perform actions that go beyond simply rendering UI. These actions—such as fetching data, accessing browser APIs, or setting up event listeners—are known as side effects. React provides the useEffect hook to handle these side effects safely.
But what changes when we use React inside a Next.js application? This post explains everything in a simple, practical way.
What Exactly Is a Side Effect?
A side effect is any action our component performs that interacts with the outside world or affects something beyond the component itself.
✔ Examples of side effects
- Fetching data from a server
- Reading or writing to
localStorage - Subscribing to scroll and keyboard events
- Starting or clearing timers
- Updating the document title
- Working with browser APIs (
window,document, etc.)
❌ Not side effects
- Rendering JSX
- Updating component state from props
- Calculating values inside render
Think of side effects as “non-UI work” our component needs to do.
Why React Uses useEffect for Side Effects
React components are designed to be pure functions: given the same state and props, they should return the same UI.
Side effects, however, are impure because they modify something outside that normal rendering process.
useEffect provides a structured way to run these side effects after React updates the DOM.
This helps React keep rendering predictable while still supporting real-world actions like API calls and event listeners.
How useEffect Works
The basic format looks like this:
useEffect(() => {
// side effect work
return () => {
// optional cleanup work
};
}, [dependencies]);
Let’s break down how it actually works.
Running an Effect Once
useEffect(() => {
console.log("Runs once when the component mounts");
}, []);
An empty dependency array ([]) tells React:
- Run this effect only once
- Ideal for initial tasks like loading data or setting up listeners
Running an Effect When Something Changes
useEffect(() => {
console.log("Count changed to:", count);
}, [count]);
The effect runs whenever count changes.
Use this when your side effect depends on specific values.
Running an Effect on Every Render
useEffect(() => {
console.log("Runs after every render");
});
Used rarely—usually only for debugging or extremely dynamic behavior.
Example: Setting the Page Title
useEffect(() => {
document.title = "Welcome!";
}, []);
This updates the browser tab title once when the component loads.
Example: Fetching Data Inside useEffect
import { useState, useEffect } from "react";
function Users() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function loadUsers() {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data = await res.json();
setUsers(data);
}
loadUsers();
}, []);
return (
<div>
<h2>Users</h2>
{users.map(u => <p key={u.id}>{u.name}</p>)}
</div>
);
}
This effect starts once and loads data from an API.
Cleanup Functions (Stopping Work When Component Unmounts)
Some side effects—like event listeners or timers—must be cleaned up.
useEffect(() => {
const timer = setInterval(() => {
console.log("Tick");
}, 1000);
return () => clearInterval(timer); // cleanup
}, []);
React runs the cleanup when the component unmounts or before re-running the effect.
Common Mistakes with useEffect
Many beginners misuse useEffect because it behaves differently from regular JavaScript functions.
Below are the most common mistakes, with clear WRONG and CORRECT examples to help you understand exactly what to do.
❌ 1. Making the effect function itself async
useEffect must return nothing or a cleanup function.
Marking it async makes it return a Promise — which React cannot use.
Wrong
useEffect(async () => {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
}, []);
Correct
Wrap the async logic inside the effect.
useEffect(() => {
async function loadData() {
const res = await fetch("/api/data");
const data = await res.json();
setData(data);
}
loadData();
}, []);
❌ 2. Forgetting the dependency array
Without a dependency array, your effect runs after every render, causing extra work or infinite loops.
Wrong
useEffect(() => {
console.log("Running too often");
setCount(count + 1); // infinite loop likely
});
Correct
Add a dependency array based on when you want it to run.
useEffect(() => {
console.log("Runs once");
}, []);
❌ 3. Putting unstable values like inline functions or objects in the dependency array
Functions and objects created inside the component are new every render, so they trigger the effect repeatedly.
Wrong
useEffect(() => {
console.log("Effect triggered");
}, [ { name: "John" } ]);
or
useEffect(() => {
console.log("Runs again");
}, [ () => console.log("Hello") ]);
Correct
Move functions/objects outside or memoize them.
Stable function
function handleAction() {
console.log("Same reference every render");
}
useEffect(() => {
handleAction();
}, []);
Or memoize
const handler = useCallback(() => {
console.log("Stable function");
}, []);
useEffect(() => {
handler();
}, [handler]);
❌ 4. Using useEffect for simple value calculations
Effects should be used only for side effects — not for computing values you can derive directly.
Wrong
useEffect(() => {
setFullName(firstName + " " + lastName);
}, [firstName, lastName]);
Correct
Compute values directly (or memoize if heavy).
const fullName = firstName + " " + lastName;
or
const fullName = useMemo(() => {
return firstName + " " + lastName;
}, [firstName, lastName]);
❌ 5. Forgetting to add all necessary dependencies (stale data issues)
Leaving something out of the dependency array creates stale values inside the effect.
Wrong
useEffect(() => {
console.log("Count:", count);
}, []); // ❌ count missing!
Correct
Include every value the effect uses.
useEffect(() => {
console.log("Count:", count);
}, [count]);
❌ 6. Updating state inside an effect without proper conditions
If the effect runs often and updates state every time, it can cause infinite loops.
Wrong
useEffect(() => {
setValue(value + 1); // updates every render
});
Correct
Only update when necessary — typically with dependencies.
useEffect(() => {
setValue(value + 1);
}, [trigger]); // updates only when "trigger" changes
How useEffect Works in Next.js
Next.js introduces a major concept:
Components are Server Components by default.
Server Components:
- Run on the server
- Do not access browser APIs
- Cannot use
useEffect - Cannot use
window,document, orlocalStorage
If we try to use useEffect in a Server Component, we’ll get an error.
To Use useEffect with NextJs, Make It a Client Component
To run browser-only logic, mark the component as a Client Component:
"use client";
Example:
"use client";
import { useEffect } from "react";
export default function ThemeLoader() {
useEffect(() => {
const theme = localStorage.getItem("theme");
console.log("Theme:", theme);
}, []);
return <div>Loading theme...</div>;
}
This component now:
- Runs in the browser
- Can use
useEffect - Can use browser APIs
When You Need useEffect in Next.js
Use useEffect in Next.js only when the work must happen in the browser:
✔ Reading from localStorage
✔ Getting window size
✔ Detecting scroll or resize events
✔ Running animations
✔ Timers
✔ Client-side analytics
✔ Anything that interacts with the actual browser
When We Should Not Use useEffect in Next.js
Avoid using useEffect for:
❌ Fetching API data
❌ Loading page content
❌ SEO meta generation
❌ Server state logic
❌ Anything using secrets or credentials
These should be done in:
- Server Components
fetch()inside Server Components- Server Actions
- Route handlers (
app/api/.../route.js)
Next.js handles these more efficiently on the server.
Final Summary
useEffect is React’s built-in tool for handling side effects—actions that occur outside the normal rendering flow.
In React, we use it for things like fetching data, setting up listeners, or interacting with browser APIs.
In Next.js, we use it only in Client Components and only for browser-only tasks.
Key ideas to remember:
✔ Side effects interact with the outside world.
✔ useEffect controls when and how these tasks run.
✔ Server Components in Next.js cannot use useEffect.
✔ Client Components ("use client") can.
✔ Most data fetching in Next.js should happen on the server.
Mastering these concepts gives ua the confidence to build robust, interactive apps using React and Next.js.
References
(Placed at the end as requested)
- React Official Docs –
useEffect
https://react.dev/reference/react/useEffect - React Official Guide – “You Might Not Need an Effect”
https://react.dev/learn/you-might-not-need-an-effect - Next.js Server & Client Components
https://nextjs.org/docs/app/building-your-application/rendering/server-and-client-components - Example usage discussions on StackOverflow
https://stackoverflow.com/questions/75149867/how-do-i-make-next-js-13-server-side-components-in-the-app-directory-that-depend