Day 4 of #100DaysOfCode — Mastering useEffect in React

Published: (February 4, 2026 at 03:58 PM EST)
5 min read
Source: Dev.to

Source: Dev.to

Understanding useEffect in React

“Unlock a magical spellbook: summon APIs, tame event listeners, and command the DOM.”

useEffect is the key to handling side effects, keeping your UI consistent, and writing logic that reacts to data changes. Below is a clear, practical breakdown of why it exists, how it works, and how to avoid dreaded infinite loops.

What Are Side Effects in React?

Side effects are any actions your component performs outside the normal rendering process.

React’s render cycle must remain predictable and pure—but many real‑world tasks aren’t pure at all, such as:

  • Fetching data from an API
  • Updating document.title
  • Working with timers (setTimeout, setInterval)
  • Using browser APIs like localStorage
  • Adding event listeners
  • Subscribing to WebSockets

These actions affect the world outside your component—those are side effects.

🤔 Why Do React Components Need useEffect?

Isn’t useState enough?

HookPurpose
useStateUpdates your UI.
useEffectHandles everything outside your UI (side effects).

If React allowed side effects during rendering, you’d get unpredictable behavior and potentially infinite loops. React must render → compare → update in a pure, deterministic way.

useEffect runs after React paints the screen, keeping the render pure and safe.

Three Types of useEffect Behavior

1️⃣ No Dependency Array – Runs on every render

useEffect(() => {
  console.log('I run after every render');
});

When to use:

  • Rarely. This runs on initial mount and on every re‑render, which can cause performance problems or infinite loops.

2️⃣ Empty Dependency Array [] – Runs only once (on mount)

useEffect(() => {
  console.log('I run only once when the component mounts');
}, []);

When to use:

  • Fetching data on mount.
  • Setting up listeners, subscriptions, or timers a single time.
  • Initialising state from localStorage or other side‑effects.

This pattern mimics componentDidMount in class components.


3️⃣ Dependency‑Based – Runs when dependencies change

useEffect(() => {
  console.log('I run when count changes');
}, [count]);

When to use:

  • Syncing derived state with props or other state values.
  • Re‑fetching data when filters, IDs, or query parameters change.
  • Updating the UI (e.g., document title) in response to a specific value.
  • Running any logic that should execute only when particular dependencies update.

This is the most common and most powerful usage of useEffect.

How Does the Dependency Array Actually Work?

The array tells React:

“Run this effect only if any of these values change from the previous render.”

React compares each dependency with its previous value using shallow comparison.

useEffect(() => {
  console.log("Runs when userId changes");
}, [userId]);

If userId goes from 12, the effect runs.

Important

  • Objects, arrays, and functions always get a new reference unless memoized (useMemo, useCallback).
  • This can cause unintentionally frequent effect runs.

How to Avoid Infinite Loops in useEffect

The most common cause

useEffect(() => {
  setCount(count + 1);
}, [count]);

What happens?

  1. setCount triggers a re‑render.
  2. count changes → the dependency array detects a change.
  3. The effect runs again → state updates again → loop forever.

How to prevent it

  • Don’t update state inside an effect that depends on that same state.
  • Use functional updates when you need to increment based on the previous value:
setCount(prev => prev + 1);
  • Or restructure the logic to avoid a state → effect → state cycle.

Cleanup Functions: What They Are and Why We Need Them

Cleanup functions run before the effect runs again or when the component unmounts.

Syntax

useEffect(() => {
  console.log("Effect started");

  return () => {
    console.log("Cleanup before re‑run or unmount");
  };
}, []);

Typical Uses

  • Removing event listeners
  • Unsubscribing from WebSockets, Firebase, etc.
  • Clearing intervals and timeouts
  • Removing observers
  • Aborting fetch requests

Example – Listener Cleanup

useEffect(() => {
  const handleResize = () => console.log("Resized");

  window.addEventListener("resize", handleResize);

  // Cleanup
  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);

Note: Without a cleanup function you can create memory leaks, stray listeners, or unwanted network activity.

Real‑World Use Cases of useEffect

1️⃣ Fetching API Data

useEffect(() => {
  async function loadData() {
    const res = await fetch("https://api.example.com/data");
    const data = await res.json();
    setItems(data);
  }

  loadData();
}, []);

2️⃣ Updating Document Title

useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);

3️⃣ Timers

useEffect(() => {
  const timer = setInterval(() => {
    console.log("Tick");
  }, 1000);

  return () => clearInterval(timer);
}, []);

4️⃣ Adding Event Listeners

useEffect(() => {
  const handler = () => console.log("Clicked");

  window.addEventListener("click", handler);

  return () => window.removeEventListener("click", handler);
}, []);

5️⃣ Working with localStorage

Save to storage whenever name changes:

useEffect(() => {
  localStorage.setItem("name", name);
}, [name]);

TL;DR

  • useEffect runs side‑effects after the component renders.
  • Choose the appropriate dependency strategy:
    • No dependencies → runs after every render.
    • Empty array [] → runs only once (on mount).
    • Specific dependencies [dep1, dep2] → runs when any listed value changes.
  • Memoize objects and functions that appear in the dependency array (e.g., with useMemo or useCallback).
  • Return a cleanup function from the effect to prevent memory leaks or stray subscriptions.
  • Avoid infinite loops by ensuring the effect does not update state that triggers the same effect again.

Happy coding! 🎉

## Fullscreen Mode

- **Enter fullscreen mode**
- **Exit fullscreen mode**

Load once on mount

useEffect(() => {
  const saved = localStorage.getItem("name");
  if (saved) setName(saved);
}, []);

Summary

ConceptMeaning
Side effectsOperations that occur outside of rendering (e.g., API calls, timers, event listeners).
Why useEffectKeeps the render pure; runs side‑effects after the UI update.
No dependency arrayRuns on every render.
Empty dependency array ([])Runs once after the component mounts.
Dependency arrayRuns when any listed value changes.
Cleanup functionUnsubscribes, removes listeners, clears timers, etc.
Common use casesFetching data, updating document title, adding/removing listeners, timers, syncing with local storage.

Final Thoughts

Learning useEffect is a turning point in becoming comfortable with React. It powers almost all real‑app functionality—from fetching data to syncing your UI with the outside world.

A deeper comprehension of useEffect supports the creation of components that scale gracefully as application complexity grows.

If you’re learning too, feel free to share your thoughts or questions below! 👇

Happy coding!

Back to Blog

Related posts

Read more »

useReducer vs useState

useState javascript const contactName, setContactName = useState''; const contactImage, setContactImage = useStatenull; const contactImageUrl, setContactImageU...

React Function and Class Components

Function Components In React, function components are defined as simple JavaScript functions that receive props properties from parent components as arguments...

Understanding `useState` in React

What Problem Does useState Solve? Before React, updating something on the screen required: - Finding the HTML element - Manually updating it - Making sure noth...