Beginner30 min read

Side Effects with useEffect

Understand side effects in React, master the useEffect hook with its dependency array, and learn common patterns for data fetching, timers, and cleanup.

What Are Side Effects?

React components are primarily responsible for computing and returning UI based on their props and state. This is the component's render logic — it should be pure, meaning it produces the same output for the same inputs without modifying anything outside itself.

However, real applications need to do more than just render UI. They need to:

  • Fetch data from an API
  • Set up subscriptions (e.g., WebSocket connections)
  • Manipulate the DOM directly (e.g., changing the document title)
  • Set timers with setTimeout or setInterval
  • Read from or write to localStorage
  • Log analytics events

These operations are called side effects because they affect things outside the component's render output. They interact with the "outside world" — the browser, the network, or other systems.

You should not perform side effects directly inside the render phase of a component. Doing so can cause bugs, infinite loops, and unpredictable behavior. Instead, React provides the useEffect hook to handle side effects safely.

jsx
// Wrong: side effect during render
function BadComponent() {
  document.title = 'Hello'; // Runs every render — not ideal
  return <p>Hello</p>;
}

// Correct: side effect in useEffect
function GoodComponent() {
  React.useEffect(() => {
    document.title = 'Hello';
  }, []);
  return <p>Hello</p>;
}

useEffect Syntax and Dependency Array

The useEffect hook takes two arguments:

  1. A setup function (the effect) that contains your side effect code.
  2. An optional dependency array that controls when the effect runs.
jsx
React.useEffect(() => {
  // Side effect code here
}, [dependencies]);

The dependency array determines when the effect re-runs:

1. Empty array [] — runs once on mount:

jsx
React.useEffect(() => {
  console.log('Component mounted!');
  // Runs once, after the first render
}, []);

This is equivalent to componentDidMount in class components. Use this for one-time setup like fetching initial data or setting up a subscription.

2. Array with dependencies [a, b] — runs when dependencies change:

jsx
React.useEffect(() => {
  console.log('Count changed to:', count);
  document.title = `Count: ${count}`;
}, [count]);

The effect runs after the first render and every time count changes. React compares the current value of each dependency with its previous value using Object.is(). If any dependency has changed, the effect runs again.

3. No dependency array — runs after every render:

jsx
React.useEffect(() => {
  console.log('Component rendered');
  // Runs after EVERY render — use sparingly!
});

This is rarely what you want. Without a dependency array, the effect runs after every single render, which can cause performance issues or infinite loops if the effect updates state.

Rules for dependencies:

  • Include every value from the component scope (props, state, variables) that the effect uses.
  • Do not lie about dependencies — if your effect uses count, include count in the array.
  • React's ESLint plugin (react-hooks/exhaustive-deps) can help catch missing dependencies.

Cleanup Functions

Some side effects need to be cleaned up when the component unmounts or before the effect re-runs. For example, if you set up a timer, you need to clear it. If you subscribe to an event, you need to unsubscribe.

The setup function can return a cleanup function. React will call this cleanup function:

  • Before the effect re-runs (when dependencies change)
  • When the component unmounts
jsx
React.useEffect(() => {
  // Setup: start a timer
  const timer = setInterval(() => {
    console.log('Tick');
  }, 1000);

  // Cleanup: stop the timer
  return () => {
    clearInterval(timer);
  };
}, []); // Empty deps: set up once, clean up on unmount

Event listeners:

jsx
React.useEffect(() => {
  function handleResize() {
    console.log('Window size:', window.innerWidth);
  }

  window.addEventListener('resize', handleResize);

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

Why cleanup matters: Without cleanup, you get memory leaks. If a component mounts and unmounts many times (e.g., navigating between pages), each mount would create a new timer or listener without removing the old ones. Over time, this degrades performance and causes bugs.

Cleanup with dependencies:

When an effect has dependencies and re-runs, React calls the cleanup from the previous effect before running the new one:

jsx
React.useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();

  return () => {
    connection.disconnect(); // Disconnect from old room
  };
}, [roomId]); // Re-runs when roomId changes

When roomId changes: disconnect from the old room (cleanup), then connect to the new room (setup).

Common Use Cases

Here are the most common patterns for useEffect:

1. Updating the document title:

jsx
function PageTitle({ title }) {
  React.useEffect(() => {
    document.title = title;
  }, [title]);

  return <h1>{title}</h1>;
}

2. Fetching data on mount:

jsx
function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);

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

    async function fetchUser() {
      const res = await fetch(`/api/users/${userId}`);
      const data = await res.json();
      if (!cancelled) {
        setUser(data);
      }
    }

    fetchUser();

    return () => {
      cancelled = true; // Prevent state update if component unmounted
    };
  }, [userId]);

  if (!user) return <p>Loading...</p>;
  return <p>{user.name}</p>;
}

The cancelled flag prevents setting state after the component has unmounted, which would cause a React warning.

3. Setting up a timer:

jsx
function Clock() {
  const [time, setTime] = React.useState(new Date());

  React.useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date());
    }, 1000);

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

  return <p>{time.toLocaleTimeString()}</p>;
}

4. Listening to browser events:

jsx
function ScrollTracker() {
  const [scrollY, setScrollY] = React.useState(0);

  React.useEffect(() => {
    const handleScroll = () => setScrollY(window.scrollY);
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return <p>Scrolled: {scrollY}px</p>;
}

5. Syncing with localStorage:

jsx
function usePersistentState(key, initial) {
  const [value, setValue] = React.useState(() => {
    const saved = localStorage.getItem(key);
    return saved !== null ? JSON.parse(saved) : initial;
  });

  React.useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}

useEffect in Action

html
<div id="root"></div>
<script src="https://unpkg.com/react@19/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@19/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
  const { useState, useEffect } = React;

  function App() {
    const [count, setCount] = useState(0);
    const [mounted, setMounted] = useState(false);

    // Effect that runs once on mount
    useEffect(() => {
      setMounted(true);
      document.title = 'useEffect Demo';
    }, []);

    // Effect that runs when count changes
    useEffect(() => {
      document.title = `Count: ${count}`;
    }, [count]);

    return (
      <div>
        <h2>useEffect Demo</h2>
        <p>Component mounted: {mounted ? 'Yes' : 'No'}</p>
        <p>Count: {count}</p>
        <button onClick={() => setCount(count + 1)}>Increment</button>
        <p><em>Check the browser tab title — it updates with the count!</em></p>
      </div>
    );
  }

  ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>

What does an empty dependency array [] in useEffect mean?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.