RE

useEffect

React · 7 entries

Basic Usage

syntax
useEffect(() => {
  // side effect
  return () => { /* cleanup */ };
}, [dependencies]);
example
import { useState, useEffect } from 'react';

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

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

Note The effect runs after the component renders. The dependency array controls when it re-runs. Omit the array: runs after every render. Empty array []: runs once on mount. With values: runs when any dependency changes.

Dependency Array

syntax
useEffect(effectFn, [dep1, dep2]);
// Empty array: mount only
useEffect(effectFn, []);
// No array: every render
useEffect(effectFn);
example
function SearchResults({ query, page }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    // Runs whenever query or page changes
    let cancelled = false;

    async function search() {
      const data = await fetchResults(query, page);
      if (!cancelled) setResults(data);
    }
    search();

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

  return <ResultsList items={results} />;
}

Note React uses Object.is to compare dependencies. Objects and arrays create new references each render, so they will always trigger the effect. Extract primitive values or memoize objects before listing them as dependencies.

Cleanup Function

syntax
useEffect(() => {
  // setup
  return () => {
    // cleanup runs before next effect and on unmount
  };
}, [deps]);
example
function ChatConnection({ roomId }) {
  useEffect(() => {
    const connection = connectToRoom(roomId);
    connection.open();

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

  return <ChatWindow />;
}

Note The cleanup function runs before the effect re-executes (when dependencies change) and when the component unmounts. This prevents memory leaks from subscriptions, timers, and open connections.

Fetching Data

syntax
useEffect(() => {
  let ignore = false;
  async function load() {
    const data = await fetchData();
    if (!ignore) setState(data);
  }
  load();
  return () => { ignore = true; };
}, [deps]);
example
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let ignore = false;
    setLoading(true);

    async function loadUser() {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      if (!ignore) {
        setUser(data);
        setLoading(false);
      }
    }
    loadUser();

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

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

Note The ignore flag prevents setting state on an unmounted component or after a stale request. In React 19, consider using the use() hook with Suspense for data fetching instead of this pattern.

Event Listeners

syntax
useEffect(() => {
  const handler = (e) => { ... };
  window.addEventListener('event', handler);
  return () => window.removeEventListener('event', handler);
}, []);
example
function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

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

  return <p>{size.width} x {size.height}</p>;
}

Note Always remove event listeners in the cleanup to avoid duplicate handlers and memory leaks. The handler reference must be the same function for addEventListener and removeEventListener.

Timers

syntax
useEffect(() => {
  const id = setInterval(() => { ... }, delay);
  return () => clearInterval(id);
}, [delay]);
example
function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const [running, setRunning] = useState(false);

  useEffect(() => {
    if (!running) return;

    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    return () => clearInterval(intervalId);
  }, [running]);

  return (
    <div>
      <p>{seconds}s</p>
      <button onClick={() => setRunning(r => !r)}>
        {running ? 'Pause' : 'Start'}
      </button>
      <button onClick={() => { setRunning(false); setSeconds(0); }}>
        Reset
      </button>
    </div>
  );
}

Note Always clear intervals and timeouts in cleanup. Use the updater form (prev => prev + 1) inside intervals to avoid stale closure issues with the state value.

When NOT to Use useEffect

syntax
// Derived state: compute during render
const fullName = firstName + ' ' + lastName;

// Event response: handle in the event handler
function handleClick() { doSomething(); }
example
// BAD: useEffect to sync derived data
const [firstName, setFirstName] = useState('Kai');
const [lastName, setLastName] = useState('Chen');
const [fullName, setFullName] = useState('');

useEffect(() => {
  setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);

// GOOD: compute directly during render
const fullName = firstName + ' ' + lastName;

// BAD: useEffect for event-driven logic
useEffect(() => {
  if (submitted) sendAnalytics();
}, [submitted]);

// GOOD: call directly in the event handler
function handleSubmit() {
  submitForm();
  sendAnalytics();
}

Note Overusing useEffect is one of the most common React mistakes. If you can calculate something from existing props or state, do it during render. If something should happen in response to a user action, put it in the event handler.