← back to section

Before 2019, logic in React could only be reused through complex constructs like render props and higher-order components. This turned components into three-level nested structures just for one shared piece of behavior. Hooks solved this problem: now any stateful logic can be extracted into an ordinary function and called from any component.

Rules of hooks

Hooks are functions whose names start with use. React knows about them thanks to two strict rules.

First rule: top level only. A hook cannot be called inside a condition, loop, or nested function.

// wrong: the order of calls will shift on each render
if (isOpen) {
  const [value, setValue] = useState("");
}

// correct: hook is always called; the condition is inside
const [value, setValue] = useState("");
if (isOpen) {
  // use value
}

React remembers each hook's state by its position in the call list. If the order changes from render to render, React loses track of which state belongs to which hook.

Second rule: React functions only. Hooks are called from components and from other custom hooks — not from regular helper functions.

The eslint-plugin-react-hooks linter plugin catches violations of both rules. Keep it enabled and don't silence its warnings.

Custom hooks

A custom hook is a regular function with a use... name, inside which other hooks are called. It is technically identical to any function, but lets you package repeated logic in one place.

For example, a search debounce is needed in several places in an application. Without a hook, that useEffect would have to be copied into every component. With a hook:

function useDebounced<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(id);
  }, [value, delayMs]);

  return debounced;
}

Now the component simply uses the hook, unaware of the details:

function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounced(query, 300);
  // debouncedQuery updates 300 ms after the user stops typing
}

Custom hooks compose well: one can call another. Logic is not duplicated, and the component stays focused on rendering.

useEffect: what it's actually for

useEffect lets you synchronize a component with something external: a subscription, a timer, manual DOM work, an external store. Here's a typical example — subscribing to a browser event:

useEffect(() => {
  function handleResize() {
    setWidth(window.innerWidth);
  }

  window.addEventListener("resize", handleResize);
  return () => window.removeEventListener("resize", handleResize);
}, []); // empty array — the effect runs once on mount

The function that the effect returns is the cleanup. It is called before the next effect run and when the component unmounts. Without cleanup, subscriptions and timers accumulate.

Dependencies: why the array matters

The second argument to useEffect is the dependency array. React re-runs the effect whenever any value in the array has changed.

useEffect(() => {
  document.title = `Hello, ${name}`;
}, [name]); // effect re-runs only when name changes

If you forget to list a variable in the array, the effect will work with a stale value. The linter warns about this — trust its fixes rather than adding // eslint-disable-line by hand.

Race conditions when loading data

If a component loads data in useEffect and the user quickly changes the input, a response to an old request may arrive on top of a fresher one already received. This is called a request race condition.

The standard protection is a cancellation flag in the cleanup function:

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

  fetchUser(userId).then((user) => {
    if (!cancelled) setUser(user);
  });

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

When userId changes, React runs the previous effect's cleanup (cancelled = true) before running the next one. The old response will arrive, but won't make it into state.

When useEffect is not needed

The most common mistake is using useEffect where it isn't needed. Three such cases:

Derived value. If a new value is computed from existing data, simply calculate it during rendering:

// no effect needed
const fullName = firstName + " " + lastName;

An effect with useState would do the same thing with an extra render and extra code.

Reacting to an event. If something should happen in response to a button click or form submission — put the logic in the event handler, not in an effect that watches for a state change.

Loading server data. A manual useEffect for fetching data from the server is roughly ten lines of code with incomplete race condition protection, no caching, and no loading state. Libraries like TanStack Query solve this reliably and concisely.

A simple question before writing an effect: "Is this synchronizing with the outside world, or reacting to my own state?" If it's the latter — no effect needed.

In short

  • Hooks are functions with a use... name that let functional components use state and side effects.
  • Two rules: call them only at the top level and only from React functions. Breaking this corrupts the call order.
  • A custom hook packages repeated logic into one function — no duplication across components.
  • useEffect is for synchronizing with the outside world: subscriptions, timers, DOM. Not for reacting to your own state.
  • A cleanup function in useEffect is required if you create a subscription or timer — otherwise you get leaks.
  • The dependency array must include everything the effect reads. The linter helps keep it honest.
  • When loading data in an effect, guard against race conditions with a cancellation flag.
  • Most useEffect calls by beginners are unnecessary: derived values are computed during rendering; reactions to events belong in handlers.
  • Components and props — how the core unit of React is structured.
  • State: local and server — which data to keep in useState and which to put in a library.
  • Data fetching — TanStack Query as a replacement for manual useEffect fetching.