How to Synchronize React with Browser APIs

Browser APIs such as events, observers, and timers live outside React. Use useEffect to connect to them and a cleanup function to disconnect.

6 min read

To synchronize React with browser APIs, useEffect connects in setup and disconnects in cleanup. Window events, observers, and timers all live outside React, so a component needs an Effect to stay attached to them. The pattern is always the same: connect in setup, disconnect in cleanup.

The cleanup is what makes the connection safe. Without it, a listener or observer keeps holding the component after it leaves, and a second one stacks on the next mount.

Connect in setup, disconnect in cleanup

React renders the UI, then runs the Effect setup. The returned cleanup runs before the next setup and on unmount.

App.jsxApp.jsx
useEffect(() => {
  const subscription = source.subscribe(handleChange);
  return () => subscription.unsubscribe();
}, []);

The subscribe and unsubscribe calls mirror each other, so the component never leaves a dangling connection. This shape applies to nearly every browser API: the cleanup undoes the setup, so React can run the pair as often as it needs without leaking. React runs the pair an extra time in development to check the symmetry.

Window events

A menu that closes on the Escape key needs a listener on window, not on any single element. A click handler on the menu cannot catch a key pressed while focus is elsewhere, so the listener must attach to window and detach when the menu unmounts.

App.jsxApp.jsx
import { useEffect } from "react";
 
function Menu({ onClose }) {
  useEffect(() => {
    const handleKey = (event) => {
      if (event.key === "Escape") onClose();
    };
    window.addEventListener("keydown", handleKey);
    return () => window.removeEventListener("keydown", handleKey);
  }, [onClose]);
  return <nav>Menu</nav>;
}

The listener is attached while the menu is mounted and detached when it closes. Because handleKey is declared inside the Effect, the same reference is passed to both add and remove. Escape to close is also a keyboard accessibility pattern, so the listener keeps the menu usable without a mouse and stays cleaned up when the menu leaves.

Observers

An IntersectionObserver watches whether an element enters the viewport. It is a browser API that lives outside React, which makes it a perfect candidate for an Effect. The cleanup calls disconnect so the observer does not keep holding the node.

App.jsxApp.jsx
import { useEffect, useRef } from "react";
 
function Section({ onVisible }) {
  const ref = useRef(null);
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) onVisible();
    }, { threshold: 1 });
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [onVisible]);
  return <div ref={ref}>Content</div>;
}

The observer watches the element for as long as the section stays mounted. Disconnecting in the cleanup releases the reference instead of leaving a hidden observer running. A forgotten disconnect keeps firing callbacks on a node the page already removed.

One-off reads need no cleanup

Not every browser API keeps running. Reading a value once, such as localStorage or the viewport size at mount, needs no cleanup because there is nothing to undo.

What to learn next

Cleaning up timers, listeners, and subscriptions drills into each cleanup pair, and React useEffect explained covers the shared setup. When the browser API must run before the paint, a layout Effect is the tool for that job.

Rune AI

Rune AI

Key Insights

  • Browser APIs live outside React and need an Effect to stay connected.
  • Connect in setup and disconnect in the cleanup function.
  • Pass the same function reference to add and remove for listeners.
  • Disconnect observers and clear timers when the component leaves.
RunePowered by Rune AI

Frequently Asked Questions

Why do browser APIs need an Effect?

They live outside React and keep running without it. An Effect connects the component to the API and a cleanup function disconnects it.

What is the cleanup for addEventListener?

removeEventListener with the same event name and function reference. Defining the handler inside the Effect guarantees the reference matches.

Do I need cleanup for every browser API?

Only for APIs that keep running, like listeners, observers, and timers. A one-off read needs no cleanup.

Conclusion

Synchronizing with browser APIs always follows one shape: connect in the Effect setup and disconnect in the cleanup. Match every addEventListener with removeEventListener and every observe with disconnect.