useEffect
In React, functional components should ideally be pure functions: given the same inputs (props), they should return the same output (JSX) without affecting anything outside their scope.
However, real-world apps need to interact with the outside world. This is called a Side Effect. Examples include:
- Fetching data from a server
- Manually modifying the DOM (e.g., setting document title)
- Setting up subscriptions (e.g., WebSockets, event listeners)
- Setting timers (
setTimeout,setInterval)
The useEffect hook lets you synchronize a component with an external system.
Basic Syntax
import { useEffect } from 'react';
useEffect(() => {
// 1. Setup code: This runs after the component renders
return () => {
// 2. Cleanup code (optional): This runs before the component is removed from the UI,
// and before the setup code re-runs on updates.
};
}, [/* 3. Dependency Array */]);
The Dependency Array
The second argument to useEffect controls when the effect runs.
- No array provided (
useEffect(() => {...})): The effect runs after every single render. (Rarely what you want). - Empty array (
useEffect(() => {...}, [])): The effect runs only once after the initial render (often compared tocomponentDidMount). Perfect for fetching initial data. - Array with dependencies (
useEffect(() => {...}, [x, y])): The effect runs after the initial render, and then only ifxoryhave changed since the last render.
function Search({ query }) {
useEffect(() => {
document.title = `Search results for ${query}`;
// This effect only re-runs if 'query' changes
}, [query]);
}
The Cleanup Function
If your effect creates resources (like an open connection, a timer, or an event listener), you must clean them up to prevent memory leaks and unexpected behavior.
You do this by returning a function from your effect.
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(intervalId); // Clear the timer when component unmounts
};
}, []); // Empty array means setup runs once, cleanup runs on unmount
return <div>Seconds: {seconds}</div>;
}
Common Mistakes
1. Missing Dependencies
If you use a variable (prop or state) inside your effect but forget to include it in the dependency array, your effect will see "stale" values from a previous render. The React Hooks ESLint plugin (eslint-plugin-react-hooks) is essential to catch this.
2. Infinite Loops
Updating state inside an effect that depends on that state (or has no dependency array) will cause an infinite loop: render -> effect -> setState -> render -> effect -> ...
3. Fetching Data Race Conditions
If a user clicks rapidly, multiple network requests can fire, and they might resolve out of order. Use a cleanup function to ignore stale responses.
useEffect(() => {
let ignore = false;
async function fetchProfile() {
const data = await api.getUser(userId);
if (!ignore) {
setProfile(data);
}
}
fetchProfile();
return () => {
// If the component unmounts OR userId changes before the fetch finishes,
// we set ignore to true so we don't update state with old data.
ignore = true;
};
}, [userId]);
Thinking in Effects
Instead of thinking about "lifecycle methods" (mount, update, unmount), think about useEffect as synchronizing. You are synchronizing the external world (the DOM, network, timers) to match the current state and props of your React component.