Effects in React

An Effect lets a component synchronize with something outside React after the screen has been updated. Use it for external systems, not for calculations React can perform while rendering.

useEffect runs after a commit

Rendering calculates what the interface should look like. React then commits the result to the DOM. After that commit, an Effect can connect the rendered component to a browser API, network connection, third-party widget, or another system React does not control.

This Effect keeps the browser tab title synchronized with the current count.
import { useEffect, useState } from "react";

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

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

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

Call Hooks only at the top level of a component or custom Hook. Do not call useEffect inside a condition, loop, or event handler. Put the condition inside the Effect when setup should sometimes do nothing.

Effect dependencies

The dependency list describes every reactive value the Effect reads. Reactive values include props, state, and variables or functions declared inside the component. React compares each dependency with its previous value using Object.is.

  • No dependency array

    useEffect(setup) runs after every commit of the component.

  • Empty dependency array

    useEffect(setup, []) does not depend on reactive values, so it normally runs once after mounting. Development Strict Mode performs an extra setup and cleanup cycle to test the logic.

  • Listed dependencies

    useEffect(setup, [roomId]) runs after mounting and again when roomId changes.

Both serverUrl and roomId are reactive and must be listed because setup reads them.
function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState(
    "https://chat.example.com"
  );

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();

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

  return (
    <label>
      Server URL
      <input
        value={serverUrl}
        onChange={event => setServerUrl(event.target.value)}
      />
    </label>
  );
}

Do not omit a dependency just to stop an Effect from rerunning. That leaves the Effect using values from an older render. Change the code so the dependency is genuinely unnecessary, or make the setup and cleanup safe to repeat.

Cleanup reverses setup

An Effect may return a cleanup function. Before React runs the Effect again with changed dependencies, it first runs the previous cleanup. React also runs cleanup when the component is removed from the screen.

Setup subscribes with one function reference, and cleanup removes that exact listener.
function OnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    function updateStatus() {
      setIsOnline(navigator.onLine);
    }

    window.addEventListener("online", updateStatus);
    window.addEventListener("offline", updateStatus);

    return () => {
      window.removeEventListener("online", updateStatus);
      window.removeEventListener("offline", updateStatus);
    };
  }, []);

  return <p>{isOnline ? "Online" : "Offline"}</p>;
}

Cleanup should mirror setup. Disconnect connections, unsubscribe listeners, clear timers, stop observations, or ignore obsolete async results. It should make a setup followed by cleanup indistinguishable from setup never having happened.

The flag prevents an older request from updating state after the query changes.
useEffect(() => {
  let ignore = false;

  async function loadResults() {
    const results = await fetchResults(query);
    if (!ignore) {
      setResults(results);
    }
  }

  loadResults();
  return () => {
    ignore = true;
  };
}, [query]);

In development, Strict Mode intentionally runs setup, cleanup, and setup again after mounting. This exposes missing cleanup. Fix the cleanup instead of using a ref to hide the second setup.

External-system synchronization

Think of each Effect as one independent synchronization process. Its setup starts or updates that process using the current reactive values, and its cleanup stops the previous process.

  • Connections and subscriptions

    Connect to a chat room, subscribe to an event source, or observe browser state. Cleanup disconnects or unsubscribes.

  • Imperative browser or third-party APIs

    Keep a map, video player, dialog, or non-React widget synchronized with props and state.

  • Network synchronization

    Fetch data when a reactive query changes and ensure an obsolete response cannot replace newer results. A framework data API or client-side cache may handle this more efficiently.

An Effect controls a video element because playback is an imperative browser operation outside React.
import { useEffect, useRef } from "react";

function VideoPlayer({ src, isPlaying }) {
  const videoRef = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      videoRef.current.play();
    } else {
      videoRef.current.pause();
    }
  }, [isPlaying]);

  return <video ref={videoRef} src={src} controls />;
}

Effects run only on the client, not during server rendering. The initial server-generated HTML must make sense before an Effect has had a chance to synchronize anything.

Avoid unnecessary Effects

If no external system is involved, an Effect is usually the wrong tool. Calculating derived data in an Effect adds an extra render with stale data. Responding to a click in an Effect loses the direct connection between the interaction and the work.

Extra synchronization

State stores fullName, then an Effect updates it whenever its two source values change.

Calculate while rendering

A regular expression creates fullName from the current props with no extra state or render.

Derived values belong in rendering. Event-specific work belongs in the event handler.
function Profile({ firstName, lastName, onSave }) {
  // Derive during rendering, not in an Effect.
  const fullName = `${firstName} ${lastName}`;

  function handleSave() {
    // The click caused this work, so keep it here.
    onSave({ firstName, lastName });
  }

  return (
    <section>
      <h3>{fullName}</h3>
      <button onClick={handleSave}>Save profile</button>
    </section>
  );
}
  • Derive values during render.

    Filter lists, combine names, and calculate booleans directly from current props and state. Use useMemo only if an expensive calculation needs caching.

  • Handle user actions in event handlers.

    Submit forms, send purchase requests, and show interaction-specific notifications where the action occurs.

  • Reset a subtree with a key when identity changes.

    If a different record should get fresh local state, render the component with that record's stable ID as its key.

  • Keep Effects for visibility-driven synchronization.

    Analytics, subscriptions, and connections often belong in Effects because being rendered on screen is what causes the synchronization.

Check your understanding

  1. An Effect reads roomId and serverUrl. Which dependencies should it list?
    Show answer

    List both reactive values: [roomId, serverUrl]. The dependency list describes the code, so every reactive value setup reads belongs there.

  2. When does React run an Effect's cleanup function?
    Show answer

    React runs the previous cleanup before rerunning setup after dependency changes, and once more when the component unmounts. Development Strict Mode also adds a setup and cleanup cycle to test the logic.

  3. Should an Effect calculate visibleItems from items and filter and store the result in state?
    Show answer

    Usually no. Calculate visibleItems during rendering so it always matches the same props and state snapshot. Consider useMemo only when the calculation is measurably expensive.