What is useEffect in React: A Deep Dive into the Side-Effect Hook

useEffect is a built-in React Hook that enables function components to perform side effects—such as data fetching, subscriptions, or manual DOM mutations—after rendering completes, with automatic cleanup execution and fine-grained dependency control.

In the facebook/react repository, useEffect serves as the primary mechanism for managing side effects within functional components. This hook integrates deeply with React's Fiber architecture to schedule work after the render commit phase, ensuring that UI updates remain synchronous while asynchronous operations run safely in the background.

How useEffect Works Under the Hood

Hook Registration in ReactHooks.js

When a component renders, useEffect calls the internal mountEffect or updateEffect implementation located in packages/react/src/ReactHooks.js. This records the effect's create function, optional cleanup, and its dependency array in the fiber's effect list.

Dependency Comparison and Scheduling

On subsequent renders, React shallow-compares the values in the dependency array. If any value has changed, the effect is marked for re-execution; otherwise React skips it entirely. This comparison happens during the render phase to determine what work is needed during the upcoming commit.

The Commit Phase in ReactFiberHooks.js

The effect is stored as a PassiveEffect in the fiber's effectList within packages/react-reconciler/src/ReactFiberHooks.js. After the DOM updates, the commitWork routine iterates over this list and invokes each effect's create function. If a previous cleanup exists from a prior render, React invokes it first, guaranteeing that side effects run after the UI is painted.

Cleanup Handling in ReactDebugHooks.js

The create function can optionally return a cleanup callback. React stores this callback in packages/react-debug-tools/src/ReactDebugHooks.js and will invoke it before the next execution of the same effect or when the component unmounts. This prevents memory leaks from stale subscriptions or pending asynchronous operations.

Scheduler Integration

Effects are scheduled with Passive priority, meaning they run after the browser has had a chance to paint. This prevents layout thrashing and keeps UI updates responsive, as heavy side effect work does not block the main thread during critical rendering phases.

Practical useEffect Examples

Data Fetching with Cancellation

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchProfile() {
      const resp = await fetch(`/api/users/${userId}`);
      const data = await resp.json();
      if (!cancelled) setProfile(data);
    }

    fetchProfile();

    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (!profile) return <p>Loading…</p>;
  return <div>{profile.name}</div>;
}

This effect runs only when userId changes. The cleanup flag prevents state updates on unmounted components, avoiding memory leaks.

Subscribing to External Events

import { useEffect } from 'react';
import { subscribeToChat, unsubscribeFromChat } from './chatApi';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const handleMessage = msg => console.log('New message', msg);
    const unsub = subscribeToChat(roomId, handleMessage);

    return () => unsub();
  }, [roomId]);
}

The effect registers a listener on mount and removes it automatically when roomId updates or the component unmounts.

Multiple Independent Effects

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('rendered with', count);
  });

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

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

The first effect runs after every render with no dependency array, while the second runs only when count changes, demonstrating fine-grained control.

Summary

  • useEffect registers side effects during rendering and executes them after the DOM updates commit.
  • The hook is implemented in packages/react/src/ReactHooks.js and integrated with the Fiber reconciler in packages/react-reconciler/src/ReactFiberHooks.js.
  • Dependency arrays control re-execution through shallow comparison; omitting them causes the effect to run after every render.
  • Cleanup functions returned from effects execute before re-runs and on unmount, preventing memory leaks and stale subscriptions.
  • Effects run with Passive priority, ensuring they do not block browser painting or cause layout thrashing.

Frequently Asked Questions

When does useEffect run relative to the render cycle?

useEffect runs after the component renders and the browser paints the DOM. According to the source code in packages/react-reconciler/src/ReactFiberHooks.js, effects are queued as PassiveEffect entries and processed during the commit phase, guaranteeing they execute only after the UI is visible to users.

Why is my useEffect running twice in development?

React intentionally double-invokes effects in development mode when using Strict Mode to test cleanup logic. As implemented in packages/react-debug-tools/src/ReactDebugHooks.js, React mounts the component, simulates an unmount by calling the cleanup function, then remounts to ensure effects handle re-initialization correctly.

Should I include functions in the dependency array?

Functions should be included if they are defined inside the component and referenced within the effect. However, since inline functions are recreated every render, you should wrap them in useCallback or define them outside the component to avoid unnecessary effect re-runs. The shallow comparison in mountEffect and updateEffect treats function references as distinct if they are new instances.

How do I fetch data properly with useEffect?

Declare the async function inside the effect or use an immediately-invoked async function, then track a cancellation flag to prevent state updates after unmount. Never make the effect callback itself async, as this would return a Promise rather than a cleanup function or undefined. The fiber architecture in packages/react-reconciler/src/ReactFiberHooks.js expects the cleanup function synchronously to properly register it for the next commit phase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s https://instagit.com/install.md

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client