Lesson 32 of 45 intermediate

Hooks

Effects connect React to the outside world, and the dependency list says when to reconnect

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

What does the function returned from a useEffect callback do?

Open interactive version (quiz + challenge)

Real-world analogy

A radio in a car needs to be tuned to the station you want. When you drive into a new area, you retune. Before you retune, you let go of the old station, so you do not hear two at once. The dependency list is the area on the map, and the cleanup is letting go of the old station. The effect is the tuning, and it runs again only when the area changes.

What is it?

Hooks are functions such as useState, useEffect and useRef that let a function component use React features. useEffect is the one for talking to things outside React. It runs after a render and re-runs when its dependencies change. The function it returns cleans up before the next run. Custom hooks combine these into reusable pieces, such as a debounced value or a data loader.

Real-world relevance

A search box that calls the API on every keystroke sends ten requests for ten letters. The slow ones can come back after the fast ones and show the wrong results. A debounce hook waits for a short pause, and the effect aborts the previous request when the query changes. A common review comment on effects is a missing dependency, which shows data for the wrong user after navigation.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

An effect reads userId but its dependency array is empty. What is the likely symptom?

Why can a hook not be called inside an if statement?

Code example

import { useEffect, useState } from 'react';

export function useDebounce<T>(value: T, delayMs: number): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(timer);
  }, [value, delayMs]);
  return debounced;
}

export function SearchBox() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);
  const [results, setResults] = useState<string[]>([]);
  useEffect(() => {
    if (!debouncedQuery) return;
    const controller = new AbortController();
    fetch('/api/search?q=' + encodeURIComponent(debouncedQuery), { signal: controller.signal })
      .then(r => {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.json() as Promise<string[]>;
      })
      .then(setResults)
      .catch(e => { if (e.name !== 'AbortError') console.error(e); });
    return () => controller.abort();
  }, [debouncedQuery]);
  return (
    <div>
      <input aria-label="Search" value={query} onChange={e => setQuery(e.target.value)} />
      <ul>{results.map(r => <li key={r}>{r}</li>)}</ul>
    </div>
  );
}

Line-by-line walkthrough

  1. 1. useDebounce is a custom hook. It keeps a copy of the value in its own state.
  2. 2. Its effect starts a timer. The cleanup clears the timer, so a fast new value cancels the old timer.
  3. 3. The dependency array lists value and delayMs, because the effect reads both.
  4. 4. SearchBox keeps the raw query for the input and the debounced query for the request.
  5. 5. The second effect runs only when debouncedQuery changes. It checks r.ok, ignores the expected AbortError, and its cleanup aborts the request that is no longer needed.
  6. 6. The input shows the raw query, so typing feels instant while the request waits for a pause.

Spot the bug

// assume: fetchName(id: number): Promise<string>, and useState/useEffect are imported
function Profile({ userId }: { userId: number }) {
  const [name, setName] = useState('');
  useEffect(() => {
    fetchName(userId).then(setName);
  }, []);
  return <h1>{name}</h1>;
}
Need a hint?
What does the effect read, and is it listed?
Show answer
The effect reads userId but the dependency array is empty, so it runs only on mount and keeps showing the first user after userId changes. Fix: list the dependency, [userId], and abort or ignore the old request in the cleanup.

Explain like I'm 5

An effect is a chore that happens after the screen is drawn, like setting a timer. The list tells React which things to watch, so it only redoes the chore when one of them changes. Before it starts again, it tidies up the old chore.

Fun fact

Hooks are matched to their state by the order they are called in, not by name. That is why a hook inside an if statement breaks a component.

Hands-on challenge

Say it out loud in 30 seconds: "What is useEffect for, and what are the common mistakes?" Then check: did you mention syncing with something outside React, the dependency array, the cleanup function, and missing dependencies?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook