Implement a useMediaQuery hook that subscribes to and responds to media query changes (e.g. screen size, resolution, orientation, etc.).
export default function Component() {const isSmallDevice = useMediaQuery('only screen and (max-width: 768px)');return <div>{isSmallDevice && <a href="#">Menu</a>}</div>;}
Hint: The window.matchMedia API is helpful.
query: string: The media query to match. It must be a valid CSS media query stringThe hook returns a boolean value that indicates whether the media query matches.
window.matchMedia(query).matches.change event and update state when it changes.query changes, unsubscribe from the old MediaQueryList and subscribe to the new one.resize listener.useMediaQuery(query) returns whether the browser currently matches a media query string. The browser owns that state through matchMedia; the hook's job is to bridge it into React by reading the current snapshot, subscribing to changes, and unsubscribing cleanly.
useMediaQuery mirrors a browser-owned value in React:
window.matchMedia(query).matches is the current snapshot.change event tells React when that snapshot may have changed.No matter which React primitive is used, the model is the same:
query changes.useEffect and useStateThis is the primary interview-friendly solution because it maps directly to common React primitives. useState stores the current match, and useEffect owns the MediaQueryList listener for the active query.
The lazy useState initializer matters because it reads .matches during the first render instead of waiting for a change event:
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);
The effect then creates a MediaQueryList for the current query, subscribes to change, and removes that same listener in cleanup. Including query in the dependency array makes React repeat that subscription lifecycle if the caller switches to a different media query.
The subscription lifecycle for a query change is:
| Render/effect step | Browser subscription |
|---|---|
first render with '(min-width: 1000px)' | read initial .matches |
| effect runs | add change listener to that MediaQueryList |
caller changes query | cleanup removes the old listener |
| next effect runs | add listener for the new MediaQueryList |
| unmount | remove the current listener |
import { useEffect, useState } from 'react';/*** @param {string} query* @returns {boolean}*/export default function useMediaQuery(query) {// Read the current snapshot immediately so the first render matches the browser state.const [matches, setMatches] = useState(() => window.matchMedia(query).matches,);useEffect(() => {// Recreate the MediaQueryList for the current query and clean up its matching listener.const mediaQueryList = window.matchMedia(query);function updateMatch() {setMatches(mediaQueryList.matches);}mediaQueryList.addEventListener('change', updateMatch);return () => {mediaQueryList.removeEventListener('change', updateMatch);};}, [query]);return matches;}
useSyncExternalStoreuseSyncExternalStore is a production-grade alternative because matchMedia behaves like an external store owned by the browser. It makes the two required operations explicit: getSnapshot reads .matches, and subscribe registers the change listener.
That adds teaching complexity. In an interview, useEffect and useState are usually the clearer starting point. In a production codebase, useSyncExternalStore can be a better fit when React's external-store semantics matter.
import { useCallback, useSyncExternalStore } from 'react';export default function useMediaQuery(query: string): boolean {// useSyncExternalStore expects a subscribe function that removes the exact listener it adds.const subscribe = useCallback((callback: () => void) => {const mediaQueryList = window.matchMedia(query);mediaQueryList.addEventListener('change', callback);return () => {mediaQueryList.removeEventListener('change', callback);};},[query],);// matchMedia is the external store; `.matches` is the current snapshot.return useSyncExternalStore(subscribe,() => window.matchMedia(query).matches,);}
false, the hook can render the wrong value until the viewport changes. Initialize state from window.matchMedia(query).matches so the first render reflects the current browser state.removeEventListener() must receive the same callback that was passed to addEventListener(). Defining the callback inside the effect keeps registration and cleanup paired for that specific MediaQueryList.
query out of the effect dependenciesWithout query in the dependency array, the hook can keep listening to an old media query after the caller passes a new one. The cleanup should run for the old query, then the effect should subscribe to the new query.
MediaQueryList.window.matchMedia is available where the hook runs.matchMedia; it does not need a separate resize listener.addEventListener('change', ...); older addListener / removeListener fallbacks would be a production compatibility add-on, not part of this prompt.console.log() statements will appear here.