useEffect and side effects

useEffect runs a function after the component renders. Use it for things that are not part of the pure render: fetching data, setting up event listeners, starting timers, connecting to a websocket. Anything that reaches outside React and affects the world.

BookList.tsx
tsx
import { useEffect, useState } from 'react';
import { BookCard } from './BookCard';

export function BookList() {
  const [books, setBooks] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    fetch('/api/books', { signal: controller.signal })
      .then((res) => res.json())
      .then(setBooks)
      .finally(() => setLoading(false));

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

  if (loading) return <p>Loading...</p>;
  return books.map((b) => <BookCard key={b.id} book={b} />);
}

Fetch books after the component mounts. Clean up on unmount.

Read the return statement inside the effect. That is the cleanup function. React calls it when the component unmounts or before the effect runs again. Use it to tear down anything the effect set up: abort fetches, clear timers, disconnect websockets. Forgetting cleanup is the most common source of memory leaks in React.

The empty array is the dependency list. An empty array means the effect runs once on mount and the cleanup runs once on unmount. A non-empty array means the effect re-runs whenever any of the listed values change. Most bugs with useEffect come from getting the dependency list wrong. Start by listing every value the effect uses from outside, and let ESLint help you catch the ones you miss.

Quiz: Quiz

Loading practice…