React useEffect Hook Explained for Beginners
What is the React useEffect hook? A beginner-friendly guide with simple code examples that explain side effects, the dependency array, and cleanup step by step.
What is the React useEffect hook? A beginner-friendly guide with simple code examples that explain side effects, the dependency array, and cleanup step by step.
If you searched for "react useEffect hook explained for beginners," here is the short answer: useEffect is a built-in React function that lets a component do something extra after it renders on the screen — like fetching data, starting a timer, or changing the page title. In this guide you will learn exactly what that means, with small code examples you can copy and run yourself.
This post assumes you already know what a React component is and how the useState hook works. If those are new to you, read our guide on the useState hook first, then come back here.
In plain English, a side effect is anything a component does that reaches outside of just showing information on the screen. Normally, a React component takes some data (props and state) and returns some HTML-like markup (JSX) to display. That is its main job.
But sometimes a component needs to do more than that. For example, it might need to:
These extra tasks are called side effects, because they happen "on the side" of the normal render process. The useEffect hook is React's official way to run this kind of code safely.
Here is the smallest possible example: a component that changes the browser tab title every time it renders.
import { useEffect } from "react";
function PageTitle() {
useEffect(() => {
document.title = "Welcome to my page!";
});
return <h1>Hello there</h1>;
}Line by line: first, we import useEffect from the "react" package, the same way you would import useState. Inside the component, we call useEffect(...) and pass it a function — this is called the effect function. Everything inside that function runs after React has finished putting the component on the screen. In this example, the effect function sets document.title, which is the browser's built-in way of changing the tab's title text.
By default, the effect function runs after every single render. That is often not what you want, so useEffect accepts a second argument called the dependency array. It controls when the effect runs.
// Runs after every render
useEffect(() => {
console.log("I run every time");
});
// Runs only once, after the first render
useEffect(() => {
console.log("I run only once");
}, []);
// Runs after the first render, and again whenever "count" changes
useEffect(() => {
console.log("count changed to", count);
}, [count]);The first useEffect has no second argument at all, so it runs after every render — this is rarely what you want, because it can slow things down. The second one passes an empty array []. An empty array means "there is nothing to watch for changes," so React only runs the effect once, right after the component first appears. The third one passes [count]. This tells React to watch the count variable, and only re-run the effect when count is different from its previous value.
| Dependency array | When the effect runs |
|---|---|
| No array at all | After every render (usually avoid this) |
| Empty array [] | Once, right after the first render |
| [value1, value2] | After the first render, and again whenever value1 or value2 changes |
The most common beginner use case is loading data from an API as soon as a component appears on screen. Here is a small example that fetches a list of users.
import { useState, useEffect } from "react";
function UserList() {
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>
);
}Here, useState([]) creates a piece of state called users that starts as an empty array, plus a function setUsers to update it. The useEffect call fetches data from a web address (URL) and, once the response arrives, converts it to JSON and passes it to setUsers. We use an empty dependency array [] because we only want this fetch to happen once, when the component first loads — not every time it re-renders. Finally, the JSX uses users.map(...) to turn each user object into a list item, and key={user.id} helps React keep track of each item efficiently.
Some effects need to be "undone" when the component disappears from the screen, or before the effect runs again. A classic example is a timer: if you start one, you should also stop it. You do this by returning a cleanup function from inside useEffect.
import { useState, useEffect } from "react";
function Clock() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
setSeconds((previous) => previous + 1);
}, 1000);
return () => {
clearInterval(timerId);
};
}, []);
return <p>Seconds elapsed: {seconds}</p>;
}setInterval(..., 1000) runs its function every 1000 milliseconds (one second), and setSeconds((previous) => previous + 1) adds one to the counter each time. The important part is the return () => { clearInterval(timerId); } line at the end of the effect. This is the cleanup function. React automatically calls it when the component is removed from the page, so the timer stops instead of running forever in the background — a common bug beginners run into when they skip cleanup.
| useState | useEffect | |
|---|---|---|
| Purpose | Stores and updates data inside a component | Runs code as a reaction to rendering or data changing |
| Returns | A value and a function to update it | Nothing directly (optionally a cleanup function) |
| Typical use | Form input, counters, toggles | Fetching data, timers, subscriptions, changing the page title |
useEffect is a React hook that lets a component run extra code — like fetching data or starting a timer — after it renders, instead of during the render itself.
Use an empty array when you want the effect to run only once, right after the component first appears on screen, such as loading initial data from an API.
In React's Strict Mode, which is on by default in new projects during development, React intentionally runs effects twice to help you catch missing cleanup functions. This does not happen in production.
No. You only need one when your effect starts something ongoing, like a timer, an event listener, or a subscription, that should be stopped when the component goes away.
You can, but for simple validation it is usually simpler and faster to check the value directly while handling the input's onChange event, and save useEffect for things that involve the world outside the component, like an API call.
New to hooks? Start with the basics of managing data in a component before diving into side effects.
Read the useState guideuseEffect can feel confusing at first, but it comes down to one idea: run this code after rendering, and optionally clean it up later. Start with the empty array [] pattern for data fetching, add specific values to the array only when you need the effect to react to changes, and always clean up timers and subscriptions. Practice with the examples above in a small project, and it will click quickly.
Building a React app and want a second pair of eyes on your architecture?
Talk to our teamOccasional, no-fluff notes on shipping modern software — startups, automation, Laravel, Shopify and more. No spam, unsubscribe anytime.
Keep reading
What is an index in SQL? A plain-English guide with copy-paste examples showing how indexes speed up queries and when you actually need one.
Learn JavaScript DOM manipulation for beginners: select elements, change text and styles, and handle clicks with simple, copy-paste code examples.
Let's build something
Book a free discovery call. We'll listen, ask sharp questions, and send you a proposal within 3 business days.