Subscriptions, Listeners, Intervals

Set Up On Mount, Tear Down On Cleanup

Subscriptions, Listeners, Intervals

Anything you start in an effect — a timer, a listener, a subscription — must be undone in the cleanup, or you leak it.

4 min read Level 2/5 #react#useEffect#listeners
What you'll learn
  • Set up a global event listener
  • Use intervals and timeouts safely
  • Subscribe to an external source

Anything that “starts” something — a listener, a timer, a subscription — needs to be torn down on cleanup. Same pattern, different APIs.

Window Event Listener

function useKeyDown(target, handler) {
  useEffect(() => {
    function onKey(e) {
      if (e.key === target) handler(e);
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [target, handler]);
}

// usage:
useKeyDown("Escape", closeModal);

Resize / Scroll

function useWindowWidth() {
  const [w, setW] = useState(window.innerWidth);
  useEffect(() => {
    function onResize() { setW(window.innerWidth); }
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);
  return w;
}

Intervals and Timeouts

function Clock() {
  const [now, setNow] = useState(new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{now.toLocaleTimeString()}</p>;
}

Websockets

function useFeed(url) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const ws = new WebSocket(url);
    ws.onmessage = e => setMessages(prev => [...prev, e.data]);
    return () => ws.close();
  }, [url]);

  return messages;
}

IntersectionObserver / ResizeObserver

function LazyImage({ src }) {
  const imgRef = useRef(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const io = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) setVisible(true);
    });
    if (imgRef.current) io.observe(imgRef.current);
    return () => io.disconnect();
  }, []);

  return <img ref={imgRef} src={visible ? src : undefined} alt="" />;
}

The pattern is always the same: start something, return the stop.

Custom Hook For Reuse

If you find yourself writing the same effect in two places, extract a custom hook (chapter 5). Subscriptions are prime custom-hook material — useMediaQuery, useOnlineStatus, useInterval, useEventListener are all examples you’ll meet in the wild.

Up Next

The [] dep array means “only on mount”. What does that really mean in practice?

Mount-Only Effects →