React cookbook · Beginner

Respond to media queries in React

Render different components for mobile and desktop and react to changes like dark mode or reduced motion.

Approach

Subscribe to window.matchMedia with useSyncExternalStore, which avoids tearing and handles subscriptions correctly. Provide a server snapshot for SSR. Prefer CSS media queries for pure styling; use the hook when behavior or rendered components differ.

Solution

import { useCallback, useSyncExternalStore } from "react";

export function useMediaQuery(query: string, serverDefault = false): boolean {
  const subscribe = useCallback(
    (onChange: () => void) => {
      const mql = window.matchMedia(query);
      mql.addEventListener("change", onChange);
      return () => mql.removeEventListener("change", onChange);
    },
    [query],
  );
  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    () => serverDefault,
  );
}

export function Navigation() {
  const isDesktop = useMediaQuery("(min-width: 1024px)");
  const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
  return isDesktop ? <Sidebar animate={!reduceMotion} /> : <BottomTabs />;
}

Pitfalls

  • Reading window during server rendering.
  • Using JS media queries for layout that CSS can handle.
  • Ignoring prefers-reduced-motion for animations.

Variations

  • Container queries in CSS for component-level responsiveness.

Further reading: react.dev: useSyncExternalStore

Reference: https://react.dev/reference/react/useSyncExternalStore

More React recipes