Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Synchronizing with Browser APIs Using useEffect and Cleanup

Welcome back. Last lesson, you kept the directory’s selected ID in state and calculated the selected user and record count during rendering. That avoided an Effect whose only job would have been to keep duplicate React state in sync.

This lesson covers the case where synchronization is needed: React cannot manage a browser-wide keyboard listener through JSX alone. You’ll use useEffect to install an Escape-key shortcut for the directory and return a cleanup function to remove it. Allow about 40 minutes for the readings, implementation, and checks.


An Effect connects React to something outside React

A button’s onClick handler runs because that particular button was clicked. A listener on window is different: it remains active independently of the button or input that currently has focus. The component must establish that subscription when it needs it and stop it when it no longer does.

An Effect runs after React commits a render, not during the rendering calculation. That matters because rendering should determine what the interface looks like, not attach listeners or modify browser APIs. React then uses the Effect’s dependencies to decide whether an existing synchronization is still valid.

Read the official React guide’s introduction to Effects and dependencies. Its video-player example uses a different browser API, but the central question is the same: what outside React must reflect the component’s current inputs?

Synchronizing with Effects

Read React’s “Synchronizing with Effects” to see why browser-API calls belong after a render and how dependencies control re-synchronization.

In “How to write an Effect,” read the three-step overview and “Step 1: Declare an Effect,” from declaring an Effect. Focus on why the video element cannot be controlled during render. Then read “Step 2: Specify the Effect dependencies,” especially the dependency example and the three-way comparison under “Pitfall.” You can stop before “Deep Dive: Why was the ref omitted from the dependency array?”

The three forms are worth keeping distinct. With no dependency array, an Effect runs after every commit. With [], it does not re-run merely because the component renders again. With [value], it re-runs after a commit when value differs from the previous render’s value. Dependencies are not a list of events you want to respond to; they describe the reactive values the Effect’s code reads.


Cleanup is part of the synchronization

Installing a listener without removing it leaves work behind. If a component disappears, its listener can still receive keyboard events. If its dependencies change, an old listener can continue acting on an outdated selection while a new one is installed.

An Effect therefore describes both sides of a subscription: its body installs the listener, and the function it returns removes that same listener. React calls cleanup before re-running an Effect whose dependencies changed, and once more when the component unmounts. The function passed to removeEventListener must be the same function object passed to addEventListener; defining the handler inside the Effect makes that pairing straightforward.

Continue in the same React guide. The chat example establishes the cleanup timing; “Subscribing to events” applies it directly to a browser listener.

Synchronizing with Effects

Continue React’s “Synchronizing with Effects” for the start-and-stop behavior you’ll use in the directory shortcut.

In “Step 3: Add cleanup if needed,” start at the opening chat-connection example and continue through cleanup timing in “How to handle the Effect firing twice in development?” Then, under “Subscribing to events,” read the event-listener pattern. Stop before “Triggering animations.”

In development, React’s Strict Mode may test an Effect with an extra setup, cleanup, setup cycle. That is a check of your cleanup, not a reason to suppress the second setup. A correctly cleaned-up subscription still leaves just one active listener.


Add an Escape shortcut to the directory

Keep the app from the previous lesson. The shortcut will be active only while its component is displayed and the current directory result contains the selected user. That last condition uses the already-derived selectedUser: an ID retained from an older result does not count as a visible selection.

Add useEffect to your existing React import. Then place this component outside App:

type SelectionShortcutProps = {
  selectedId: string | null
  onClear: (nextId: string | null) => void
}

function SelectionShortcut({
  selectedId,
  onClear,
}: SelectionShortcutProps) {
  useEffect(() => {
    if (selectedId === null) return

    function handleKeyDown(event: KeyboardEvent) {
      if (event.key === 'Escape') {
        onClear(null)
      }
    }

    window.addEventListener('keydown', handleKeyDown)

    return () => {
      window.removeEventListener('keydown', handleKeyDown)
    }
  }, [selectedId, onClear])

  return (
    <p>
      {selectedId === null
        ? 'Select a user in the current result to enable Escape.'
        : 'Press Escape to clear the selection.'}
    </p>
  )
}

KeyboardEvent here is the browser event type delivered by window.addEventListener, not React’s event-handler type for a JSX element. The Effect reads selectedId to decide whether a subscription is needed and reads onClear inside the handler, so both appear in its dependency array. If selectedId is null, returning no cleanup function is appropriate: nothing was installed.

Inside App, add a visibility control alongside the existing state declarations:

const [showShortcut, setShowShortcut] = useState(true)

In the directory-preview <section>, after the selected-user display and rename button from the previous lesson, add:

<button
  type="button"
  aria-pressed={showShortcut}
  onClick={() => setShowShortcut((current) => !current)}
>
  Keyboard shortcut: {showShortcut ? 'On' : 'Off'}
</button>

{showShortcut && (
  <SelectionShortcut
    selectedId={selectedUser?.id ?? null}
    onClear={setSelectedId}
  />
)}

Passing setSelectedId directly gives onClear a stable function identity. If unrelated state—such as search text—causes App to render again, it does not by itself require a new keyboard subscription.

Notice how the browser API and React state have different roles. selectedId remains the source of the user’s selection; the Effect does not calculate or store another selected user. It only keeps an external listener aligned with whether a current selection can be cleared.


Trace the listener, then check the app

With the shortcut on, load a populated result and select a user. The Effect installs one keydown listener. Selecting a different user changes the selectedId prop: React removes the previous listener before installing the next one. Pressing Escape clears the ID; the next render passes null, and cleanup removes the listener without replacing it.

The visibility button provides a separate check. Select a user, switch the shortcut off, and press Escape: the selection should remain because unmounting SelectionShortcut removed its listener. Switch it back on, and Escape should clear the selection. If you remove the selected user from the current result, the component remains visible but receives null; the listener is removed even though the underlying selected ID is still retained, as established last lesson.

You can temporarily add logs beside addEventListener and removeEventListener to observe setup and cleanup, then remove them. Expect an extra setup-and-cleanup pair in development with Strict Mode. What matters is that repeated toggles or selections do not accumulate active listeners.

Run the type check after editing:

npm run typecheck

A global Escape shortcut is useful for demonstrating synchronization, but its scope is broad: it also hears Escape while an input has focus. In a fuller interface, you would decide whether that interaction should clear the selection before keeping a window-wide shortcut.


What to carry forward

Use an Effect when a component must keep an external system aligned with its current props or state—not to maintain values that can be calculated during render. For a browser subscription, install the listener in the Effect, declare the reactive values it reads, and return cleanup that removes the same listener. Cleanup handles both changing dependencies and unmounting.

This completes the React and TypeScript foundations module. Next, you’ll organize the growing app into layers by responsibility, so features such as this shortcut have a clear place in a maintainable project.

Can't find a good explanation? Sign up and we'll make it for you

Sign up