useWindowSize

Layout decisions CSS cannot make — how many rows a virtualized list should render, whether to mount a drawer or a sidebar — need the viewport size as a JavaScript value. Subscribe once in a Hook and every component that asks gets a number that stays current.

Subscribing to the viewport sizeJavaScript
import { useState, useEffect } from 'react';
const readSize = () => ({ width: window.innerWidth, height: window.innerHeight });
export function useWindowSize() {
  const [size, setSize] = useState(readSize);   // lazy: real numbers on the first render
  useEffect(() => {
    const onResize = () => setSize(readSize());
    window.addEventListener('resize', onResize);
    onResize();                                 // catch a resize during hydration
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return size;
}

A component writes const { width } = useWindowSize(); and branches on it, rendering a tab bar below 700 pixels and a sidebar above.

Passing readSize itself to useState — not readSize() — makes it a lazy initializer (Lazy State Initialization), so the first paint already has real numbers and there is no flash of 0 x 0. The listener is registered once under an empty dependency array and removed in the cleanup, so mounting the Hook in twenty components costs twenty listeners, not twenty per render.

Two cautions. window does not exist while a server renders, so in Next.js 10,514 guard the initializer or read the value through useSyncExternalStore, which takes a separate server snapshot. And resize fires on every pixel of a drag: if the render is expensive, feed the value through useDebounce (useDebounce) or switch to window.matchMedia('(min-width: 700px)'), which fires only when the answer changes. When the decision is purely visual, no Hook belongs in it at all — a CSS media query costs zero renders.