Skip to main content

What are side effects?

React components have one job: return JSX based on props and state. Anything else — fetching data, updating the document title, setting up a timer — is a side effect. It’s something that happens outside the render cycle.
useEffect is the hook for running side effects. In this course, you’ll use it mostly for fetching data and syncing with browser APIs (like document title or timers).
Python mental model: your component function is like a function React may call many times to calculate UI. Keep that part “pure” (return JSX from props/state). useEffect is where you register code that should run after render to sync with the outside world (network, browser APIs, timers).

Basic useEffect

useEffect takes two arguments:
  1. A function — the code to run (the effect)
  2. A dependency array — when to re-run it

The dependency array

The dependency array controls when the effect runs:
Python mental model: the dependency array is the list of values React watches. You’re telling React, “re-run this sync code when these inputs change.”
In development, React.StrictMode may run mount effects twice to help reveal bugs. For a simple useEffect(..., []) data-fetch example, that can mean two requests in development unless you guard against it. Production does not do the extra development check.
[] is a common beginner pattern for “run on mount” effects (like an initial fetch), but it’s not the default for every effect. As a rule: put every prop/state value your effect uses into the dependency array. Omitting the array entirely causes the effect to run on every render, which is usually a bug.

Fetching data on mount

This is the pattern you’ll use most often — load data when a component first appears:
Notice the async function is defined inside the useEffect callback, not as the callback itself. This is because useEffect callbacks can’t be async directly — they need to return either nothing or a cleanup function.

Fetching based on a prop or state value

When the data depends on a value (like a user ID from the URL), include it in the dependency array:
When userId changes (e.g., navigating from one user profile to another), the effect runs again and fetches the new user.

Other common side effects

Updating the document title

Setting up a timer

Cleanup function

The function you return from useEffect runs when the component unmounts (disappears) or before the effect re-runs:
Cleanup prevents memory leaks and stale data. Common cleanup tasks:
  • Clearing timers (clearInterval, clearTimeout)
  • Closing WebSocket connections
  • Cancelling pending requests
  • Removing event listeners
You won’t need cleanup for most simple fetch-on-mount examples. Cleanup is mainly for subscriptions, timers, and event listeners. In development, StrictMode may re-run effects and expose problems earlier, but don’t rely on it to catch every cleanup bug.

Common mistakes

Forgetting the dependency array creates an infinite loop: render → effect → state update → re-render → effect → … Your browser tab will freeze and your API will get hammered.
useEffect callbacks must return either nothing or a cleanup function. An async function returns a Promise, which React doesn’t know how to handle. Define the async function inside the effect and call it immediately.
Objects and arrays are compared by reference, not by value. {a: 1} !== {a: 1}. Use primitive values (strings, numbers, booleans) in the dependency array whenever possible.

The useEffect mental model

Think of useEffect in terms of synchronization, not lifecycle:
  • useEffect(fn, []) — synchronize with nothing (run once)
  • useEffect(fn, [userId]) — synchronize with userId (re-run when it changes)
  • useEffect(fn, [a, b]) — synchronize with a and b
If you’re coming from Python or other frameworks, resist thinking in “component lifecycle” terms (mount, update, unmount). Think instead: “this effect should run when these values change.” The dependency array is the key to understanding useEffect.
Also remember: useEffect callbacks are closures. They capture values from the render where they were created. That’s why missing a dependency can make an effect use an old value (“stale closure”).

What’s next?

You can fetch data and manage side effects. Now let’s learn how to conditionally show different UI based on that data — loading spinners, error messages, and content.

Conditional rendering

Show different UI based on state and conditions