Skip to content

React useEffect Explained — Side Effects in Function Components

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about React useEffect Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

React useEffect lets you perform side effects in function components: fetching data, subscribing to events, manipulating the DOM, and synchronizing with external systems.

What You'll Learn

  • What side effects are and why they need special handling
  • How useEffect runs after render
  • How dependency arrays control when effects run
  • How to clean up effects to prevent memory leaks
  • Common useEffect patterns

Why It Matters

Components should be pure functions of props and state. Side effects break that purity. useEffect provides a dedicated place for side effects, keeping the render phase pure and predictable.

Real-World Use

Durga Antivirus Pro uses effects to start file system watchers, subscribe to threat detection events via Websocket, update the scan progress bar, and synchronize preferences with localStorage.

flowchart LR
    A[Component Renders] --> B[Browser Paints DOM]
    B --> C[useEffect Runs]
    C --> D[Side Effect: API Call]
    C --> E[Side Effect: Subscribe]
    C --> F[Side Effect: DOM Update]
    D --> G[Set State] --> A
    style A fill:#3b82f6,color:#fff

Basic useEffect

The most common side effect is fetching data:

import { useState, useEffect } from "react";

function ProductList({ category }) {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    console.log("Fetching products for category:", category);

    fetch(`/api/products?category=${category}`)
      .then(res => res.json())
      .then(data => {
        setProducts(data);
        setLoading(false);
      })
      .catch(err => {
        console.error("Failed to fetch products:", err);
        setLoading(false);
      });
  }, [category]);

  if (loading) return <div>Loading products...</div>;

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name} - ${product.price}</li>
      ))}
    </ul>
  );
}

Expected output: Products load when the component mounts. Changing the category re-fetches products.

The effect runs after every render where category changed. React guarantees the DOM has been updated before effects run, so you can safely read from or write to the DOM.

Effect with Cleanup

Cleanup functions prevent memory leaks:

import { useState, useEffect } from "react";

function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    console.log("Adding mouse listener");
    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      console.log("Removing mouse listener");
      window.removeEventListener("mousemove", handleMouseMove);
    };
  }, []);

  return (
    <div>
      <p>Mouse position: {position.x}, {position.y}</p>
    </div>
  );
}

function MouseToggle() {
  const [visible, setVisible] = useState(true);

  return (
    <div>
      <button onClick={() => setVisible(!visible)}>
        {visible ? "Hide" : "Show"} Tracker
      </button>
      {visible && <MouseTracker />}
    </div>
  );
}

Expected output: When visible, mouse position updates. When hidden, the event listener is removed. Showing it again adds a fresh listener.

The cleanup function runs when the component unmounts or before the effect re-runs. Every effect that creates a subscription, timer, or listener must return a cleanup.

Multiple Effects

Separate unrelated concerns into different useEffect calls:

function Dashboard({ userId }) {
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);
  const [theme, setTheme] = useState(() => {
    return localStorage.getItem("theme") || "light";
  });

  // Effect 1: Fetch user data
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(setUser);
  }, [userId]);

  // Effect 2: Subscribe to notifications
  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/notifications?userId=${userId}`);
    ws.onmessage = (event) => {
      setNotifications(prev => [...prev, JSON.parse(event.data)]);
    };
    return () => ws.close();
  }, [userId]);

  // Effect 3: Sync theme to localStorage
  useEffect(() => {
    localStorage.setItem("theme", theme);
    document.body.className = theme;
  }, [theme]);

  // Effect 4: Set up keyboard shortcut
  useEffect(() => {
    const handler = (e) => {
      if (e.key === "Escape") setNotifications([]);
    };
    window.addEventListener("keydown", handler);
    return () => window.removeEventListener("keydown", handler);
  }, []);

  return <div>...</div>;
}

Expected output: Four independent effects, each handling a specific side effect. They do not interfere with each other.

Each useEffect handles one concern. This makes the code easier to reason about and the dependency arrays more precise.

Effect with DOM Measurements

Effects can read from the DOM after render:

import { useState, useEffect, useRef } from "react";

function TextExpander({ children, maxHeight = 100 }) {
  const [expanded, setExpanded] = useState(false);
  const [needsExpand, setNeedsExpand] = useState(false);
  const contentRef = useRef(null);

  useEffect(() => {
    if (contentRef.current) {
      const scrollHeight = contentRef.current.scrollHeight;
      setNeedsExpand(scrollHeight > maxHeight);
    }
  }, [maxHeight, children]);

  return (
    <div>
      <div
        ref={contentRef}
        style={{
          maxHeight: expanded ? "none" : `${maxHeight}px`,
          overflow: "hidden",
          transition: "max-height 0.3s ease"
        }}
      >
        {children}
      </div>
      {needsExpand && (
        <button onClick={() => setExpanded(!expanded)}>
          {expanded ? "Show Less" : "Show More"}
        </button>
      )}
    </div>
  );
}

Expected output: If content exceeds maxHeight, a "Show More" button appears. Clicking it expands the content.

Reading the DOM is a side effect that must happen after the browser has laid out the DOM. useEffect guarantees the paint is complete before it runs.

Effect with Debounce

Debounce effect execution to avoid expensive operations:

import { useState, useEffect } from "react";

function SearchAutocomplete() {
  const [query, setQuery] = useState("");
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    if (!query || query.length < 2) {
      setSuggestions([]);
      return;
    }

    const timer = setTimeout(async () => {
      const response = await fetch(`/api/search?s=${query}`);
      const data = await response.json();
      setSuggestions(data);
    }, 300);

    return () => clearTimeout(timer);
  }, [query]);

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {suggestions.length > 0 && (
        <ul>
          {suggestions.map(s => <li key={s.id}>{s.title}</li>)}
        </ul>
      )}
    </div>
  );
}

Expected output: The API call is debounced by 300ms. If the user types within 300ms, the previous timer is cleared and a new one starts.

The cleanup function clears the previous timer, effectively debouncing the effect. This pattern prevents excessive API calls during rapid typing.

Common Mistakes

  1. Missing dependencies — Using variables inside an effect that are not listed in the dependency array causes stale closures.

  2. Unnecessary dependencies — Including functions or objects in dependencies can cause infinite loops. Use useCallback for functions.

  3. Forgetting cleanup — Timers, subscriptions, listeners, and fetch requests without cleanup cause memory leaks and warnings.

  4. Running effects synchronously — Effects are async by nature. Do not use them for sync operations that belong in the render phase.

  5. Fetching without handling race conditions — If the effect runs again before a previous fetch completes, state may update with stale data. Use cleanup to abort.

Practice Questions

  1. When does useEffect run? After every completed render, but only if one of the dependencies has changed since the last render.

  2. What does the dependency array control? It tells React when to re-run the effect. Only run when specific values change.

  3. When does the cleanup function execute? Before the component unmounts and before each re-execution of the effect.

  4. How do you make useEffect run only once? Pass an empty dependency array []. The effect runs only after the initial render.

  5. Why should effects be about a single concern? Multiple concerns in one effect make dependencies harder to manage and effects less reusable.

Challenge

Build a InfiniteScroll component that loads more data as the user scrolls. Use useEffect to attach a scroll listener with IntersectionObserver. Clean up the Observer on unmount. Debounce the scroll handler to avoid excessive checks.

FAQ

Can useEffect be async?

The effect function itself cannot be async, but you can define an async function inside it: useEffect(() => { async fn() {...}; fn(); }, []).

What is the difference between useEffect and useLayoutEffect?

useEffect fires after paint (non-blocking). useLayoutEffect fires before paint (blocking). Use useLayoutEffect only for DOM measurements.

Can I skip effects with React.memo?

Yes, React.memo prevents re-renders, which also prevents effects from re-running. Only affects parent-triggered re-renders.

How do I handle errors in effects?

Use try/catch inside the effect or .catch() on promises. Set error state to display error UI.

What happens if an effect throws?

React catches the error in development. In production, the error propagates. Use error boundaries for graceful handling.

Mini Project

Build a RealtimeDashboard with multiple data sources. Use separate effects for: fetching initial data, subscribing to a WebSocket for live updates, syncing state to localStorage, and resizing charts on window resize. Each effect should have proper cleanup. Simulate the WebSocket with setInterval that generates random data points. Display connection status that reacts to effects running.

What's Next

Continue with refs and context:

React Refs, React Context, React Hooks

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro