React useEffect: Side Effects, Dependencies, and Cleanup

Learn React useEffect: Side Effects, Dependencies, and Cleanup with practical examples, explanations, best practices, and real-world coding techniques.

Introduction to the useEffect Hook

The useEffect Hook lets a React function component synchronize with external systems. These systems include browser APIs, event subscriptions, timers, third-party widgets, and network connections. An effect runs after React commits a render and can return a cleanup function to undo or stop the synchronization when it is no longer needed.

Effects are useful when a component must keep something outside React synchronized with its current props or state. However, not every operation belongs in an effect. Calculations that depend only on props and state can usually happen during rendering, while actions caused directly by a user interaction generally belong in an event handler.

Before exploring effects, understand React fundamentals, state management, and the Rules of Hooks.

Basic useEffect Syntax

The useEffect Hook accepts a setup function and an optional dependency array. The setup function contains the synchronization logic. If it returns another function, React uses that function for cleanup.

import { useEffect } from 'react';

useEffect(() => {
  // Synchronize with an external system.

  return () => {
    // Clean up the synchronization.
  };
}, [dependency]);

React runs the setup function after the component commits. When dependencies change, React first runs the previous cleanup function, then runs the new setup function. When the component is removed, React runs the final cleanup.

1. Effect Without a Dependency Array

If you omit the dependency array, React runs the effect after every committed render of the component. This can be useful in specific synchronization scenarios, but it may also repeat unnecessary work if the effect is not carefully designed.

import { useEffect, useState } from 'react';

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

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

  return (
    <section>
      <p>Count: {count}</p>
      <button onClick={() => setCount(value => value + 1)}>
        Increment
      </button>
    </section>
  );
}

Each click updates the count and causes another render, after which the effect logs the current value. The effect also runs after the initial committed render. Avoid using this pattern as a default way to calculate values or trigger additional state updates because it can create unnecessary rendering cycles.

2. Effect with an Empty Dependency Array

An empty dependency array tells React that the effect has no reactive dependencies that require re-synchronization. React runs its setup after the initial commit and runs its cleanup when the component is removed. In development Strict Mode, React performs an additional setup-and-cleanup cycle to help reveal bugs.

import { useEffect } from 'react';

function WelcomeMessage() {
  useEffect(() => {
    console.log('Welcome component mounted');

    return () => {
      console.log('Welcome component cleaned up');
    };
  }, []);

  return <h2>Welcome to React</h2>;
}

This pattern is useful for synchronization that does not depend on changing component values. It should not be interpreted as a guarantee that setup runs exactly once in every development environment, because Strict Mode intentionally checks effect behavior.

3. Effect with Dependencies

A dependency array specifies which reactive values an effect uses. When one of those values changes between renders, React runs the previous cleanup and then executes the effect again with the new values.

import { useEffect, useState } from 'react';

function PageTitle() {
  const [title, setTitle] = useState('React Basics');

  useEffect(() => {
    document.title = title;
  }, [title]);

  return (
    <label>
      Page title
      <input
        value={title}
        onChange={event => setTitle(event.target.value)}
      />
    </label>
  );
}

When the user changes the title, React updates the state and renders the component again. The effect then synchronizes the browser document title with the latest state. Because title is a reactive value used by the effect, it belongs in the dependency array.

Understanding the Dependency Array

The dependency array controls when React re-synchronizes an effect. React compares each dependency with its previous value using Object.is. When a dependency differs, React runs the cleanup associated with the previous setup and then runs the new setup.

Pattern When the effect runs Typical use
useEffect(setup) After every committed render. Synchronization that intentionally follows every render.
useEffect(setup, []) After the initial commit, with cleanup on removal; development Strict Mode adds a test cycle. Synchronization without changing reactive dependencies.
useEffect(setup, [value]) After the initial commit and when a dependency changes. Synchronizing an external system with current state or props.

Dependencies should reflect the reactive values used by the effect. Do not remove dependencies merely to force the effect to run less often. If dependencies cause unnecessary re-synchronization, restructure the effect or move non-reactive calculations outside it where appropriate.

Cleanup Functions in useEffect

Cleanup prevents an effect from leaving behind resources such as event listeners, timers, subscriptions, or connections. React invokes cleanup before re-running an effect whose dependencies changed and when the component is removed.

import { useEffect, useState } from 'react';

function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function updateWidth() {
      setWidth(window.innerWidth);
    }

    window.addEventListener('resize', updateWidth);
    updateWidth();

    return () => {
      window.removeEventListener('resize', updateWidth);
    };
  }, []);

  return <p>Window width: {width}px</p>;
}

The setup function registers a resize listener, and the cleanup function removes the same listener. Without cleanup, repeated mounts or effect re-synchronization could leave unnecessary listeners active.

This example assumes the component runs in a browser. In applications that render components on the server, browser-only values such as window should not be accessed during server rendering; initialize them in a browser-safe way when necessary.

Fetching Data with useEffect

An effect can synchronize a component with an API request when a value such as a user ID changes. A common implementation tracks loading, success, and error states and uses an AbortController to cancel a request when it is no longer needed.

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUser() {
      setLoading(true);
      setError('');
      setUser(null);

      try {
        const response = await fetch(
          'https://api.example.com/users/' + userId,
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error('Unable to load user information.');
        }

        const data = await response.json();
        setUser(data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message || 'An unexpected error occurred.');
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    }

    loadUser();

    return () => {
      controller.abort();
    };
  }, [userId]);

  if (loading) {
    return <p role="status">Loading user...</p>;
  }

  if (error) {
    return <p role="alert">{error}</p>;
  }

  if (!user) {
    return <p>No user information available.</p>;
  }

  return <h2>{user.name}</h2>;
}

This example cancels the previous request when the component is removed or the user ID changes. The API URL is illustrative and must be replaced with a real endpoint. In production code, validate response data and handle the error types expected by your API.

Direct data fetching in effects can require substantial boilerplate and does not automatically provide caching, request deduplication, or server rendering support. For larger applications, consider a framework's data-loading features or a dedicated data-fetching library. See the React API fetching guide.

Avoiding Infinite Effect Loops

An effect can create an infinite update loop when it updates state and that update causes the effect to run again without a condition that allows the cycle to stop. This often happens when dependencies are missing, unstable objects are recreated on each render, or state is being used to store a value that can be calculated directly.

Consider this example:

// Avoid this pattern.
useEffect(() => {
  setCount(count + 1);
}, [count]);

Each effect execution increments the count, which changes the dependency and causes another execution. If you intend to increment the count after a button click, perform the update in the event handler instead. If you intend to calculate a derived value, calculate it during rendering.

function CartSummary({ items }) {
  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return <p>Cart total: ₹{total}</p>;
}

The total is derived directly from the input data, so it does not need a separate state variable or effect to keep it synchronized.

Working with Objects and Functions in Dependencies

React compares effect dependencies using Object.is. An object or function created during rendering usually has a new reference on the next render, even when its contents look identical. Including such a value in the dependency array may cause an effect to re-run more frequently than intended.

import { useEffect } from 'react';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: 'https://chat.example.com',
      roomId: roomId
    };

    const connection = createConnection(options);
    connection.connect();

    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return <h2>Chat room: {roomId}</h2>;
}

In this example, the options object is created inside the effect, so it does not need to be a dependency. The effect depends on roomId, which determines the connection that should be established. The createConnection function represents an application-specific API.

Prefer restructuring the effect before reaching for memoization. If a function or object genuinely needs a stable identity for other reasons, useMemo or useCallback may help, but they should not be used to conceal missing dependencies.

When Should You Avoid useEffect?

Effects are designed for synchronization with external systems. Using them for ordinary calculations or user-triggered actions can introduce unnecessary state, additional renders, and difficult-to-follow behavior.

  • Derived values: Calculate filtered lists, totals, and formatted labels during rendering when they can be computed from existing props or state.
  • User actions: Handle button clicks and form submissions in event handlers rather than using an effect to detect that a state value changed because of a click.
  • State initialization: Initialize state with useState instead of copying a prop into state through an effect unless you intentionally need to manage a separate value.
  • Simple transformations: Use ordinary JavaScript expressions and functions instead of creating effects for synchronous calculations.
  • Shared state: Consider lifting state to a suitable parent or using context when multiple components need the same source of truth.

Common useEffect Mistakes

  • Omitting dependencies: Effects may read stale values when reactive dependencies are missing.
  • Adding unnecessary dependencies: Unstable object or function references can cause repeated synchronization.
  • Forgetting cleanup: Event listeners, timers, and subscriptions can remain active longer than intended.
  • Using effects for derived state: Duplicated state can become inconsistent with its source values.
  • Ignoring request cancellation: An outdated asynchronous operation may complete after the component no longer needs its result.
  • Assuming an empty dependency array means exactly once everywhere: Development Strict Mode performs an extra setup-and-cleanup cycle.
  • Making the effect callback async: An async function returns a Promise, not the cleanup function expected by React. Define an async function inside the effect and call it instead.

Best Practices for useEffect

  • Use effects to synchronize with external systems rather than to orchestrate every state update.
  • Include every reactive value used by the effect in its dependency array.
  • Return cleanup functions when subscriptions, listeners, timers, or connections must be stopped.
  • Keep setup and cleanup logically paired so repeated setup remains safe.
  • Use request cancellation or an appropriate stale-result strategy for asynchronous work.
  • Keep effects focused on one synchronization responsibility.
  • Use the React Hooks lint rules to identify dependency and Hook-order issues.
  • Test behavior under Strict Mode to expose effects that are not safe to set up and clean up repeatedly.
  • Prefer framework data-loading features or data-fetching libraries when they better address caching and request coordination.

Conclusion

The React useEffect Hook is useful for synchronizing components with browser APIs, subscriptions, timers, network requests, and other external systems. Understanding dependency arrays and cleanup functions helps you prevent stale values, unnecessary work, and resource leaks.

The most important principle is to use effects when synchronization is necessary, not as a default mechanism for calculating values or responding to user interactions. Continue learning with useRef, custom Hooks, and API data fetching.

Code Examples

import { useEffect, useState } from "react";

export default function PageTitle() {
  const [title, setTitle] = useState("React Basics");

  useEffect(() => {
    document.title = title;
  }, [title]);

  return (
    <input
      value={title}
      onChange={event => setTitle(event.target.value)}
      aria-label="Page title"
    />
  );
}
    
import { useEffect, useState } from "react";

export default function WindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function updateWidth() {
      setWidth(window.innerWidth);
    }

    window.addEventListener("resize", updateWidth);
    updateWidth();

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

  return <p>Window width: {width}px</p>;
}
    
import { useEffect, useState } from "react";

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    async function loadUser() {
      setLoading(true);
      setError("");

      try {
        const response = await fetch(
          "https://api.example.com/users/" + userId,
          { signal: controller.signal }
        );
        if (!response.ok) throw new Error("Request failed");
        setUser(await response.json());
      } catch (err) {
        if (err.name !== "AbortError") {
          setError(err.message || "Unable to load user");
        }
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    }

    loadUser();
    return () => controller.abort();
  }, [userId]);

  if (loading) return <p role="status">Loading...</p>;
  if (error) return <p role="alert">{error}</p>;
  if (!user) return <p>No user found.</p>;
  return <h2>{user.name}</h2>;
}
    
export default function CartSummary({ items }) {
  const total = items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return <p>Cart total: ₹{total}</p>;
}
    

Pro Tip

Before adding an effect, ask whether the component is synchronizing with something outside React. If you are only calculating a value from props or state, calculate it during rendering. For effects that do synchronize with external systems, include the correct dependencies and make cleanup mirror setup.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.