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
windowduring server rendering. - Using JS media queries for layout that CSS can handle.
- Ignoring
prefers-reduced-motionfor 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
- Add optimistic updates to a todo list
- Build an accessible modal dialog
- Build infinite scroll with TanStack Query
- Call a cookie-authenticated API with CSRF protection
- Debounce a search input in React
- Handle errors gracefully with error boundaries
- Implement dark mode that follows the system
- Poll a long-running job's status