useEffect & Side Effects

React components should be pure during render — same input, same output, no side effects. But real apps need to fetch data, subscribe to events, and set timers. That's what useEffect is for.

The Basic Pattern

basic.jsx
import { useState, useEffect } from 'react';

function PageTitle({ title }) {
  // Run after every render where title changed
  useEffect(() => {
    document.title = title;
    console.log('Title updated to:', title);
  }, [title]); // dependency array

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

The Dependency Array

The second argument to useEffect controls when it runs:

dependencies.jsx
import { useEffect } from 'react';

// No dependency array: runs after EVERY render
useEffect(() => {
  console.log('Runs after every render');
});

// Empty array []: runs ONCE after first render (mount)
useEffect(() => {
  console.log('Runs once on mount');
}, []);

// With values: runs when those values change
useEffect(() => {
  console.log('userId changed to:', userId);
}, [userId]);

useEffect(() => {
  console.log('query or page changed');
}, [query, page]);

Fetching Data

The most common use case — fetching data when a component mounts:

fetch.jsx
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    setLoading(true);
    setError(null);

    fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('User not found');
        return res.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [userId]); // Re-fetch when userId changes

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Cleanup Functions

Effects can return a cleanup function that runs before the next effect or when the component unmounts:

cleanup.jsx
import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    // Cleanup: clears interval when component unmounts
    // or before the effect runs again
    return () => clearInterval(interval);
  }, []); // Run once

  return <p>Elapsed: {seconds}s</p>;
}

function ChatRoom({ roomId }) {
  useEffect(() => {
    const socket = connectToRoom(roomId);

    return () => {
      socket.disconnect(); // Cleanup on unmount or roomId change
    };
  }, [roomId]);

  return <div>Chat room: {roomId}</div>;
}
??

Think of useEffect as "after each render, do this". The dependency array narrows "after each render" to "after renders where these values changed".