In this post, I walk you through how to subscribe a media query change in React
creating a custom hook called useMediaQuery hook using useSyncExternalStore
hook.
What is useSyncExternalStore
Since React 18, useSyncExternalStore is available to subscribe external stores
outside React. We can also levaledge this hook to subscribe changes caused by
browser APIs.
Create useMediaQuery hook
Assume that you're using TypeScript. useMediaQuery takes a media query string
as an argument and returns a boolean value whether the media query string
matches and also returns undefined if window is not available.
// useMediaQuery.ts
import { useSyncExternalStore } from "react";
export default function useMediaQuery(mediaQueryString: string) {
// The logic goes here.
}
useSyncExternalStore accepts three arguments.
- The first argument is the
subscribefunction that subscribes the external store changes. It accepts a callback as an argument that will be called when the store changes. In this case, aMediaQueryListobject listens thechangeevent invoked when the media query status of the document changes. - The second argument is the
getSnapshotfunction that sets the value returned fromuseSyncExternalStoreand is called when thesubscribefunction's callback is invoked. In this case, it returns thebooleanvalue whether themediaQueryStringmatches or not. - The third argument is the
getServerSnapshotfunction that returns the initial value to be set during the hydration and can be omitted if the components that use this hook are fully rendered on the client. In this case,undefinedis returned sincewindowis not available on the server.
For more details, read the API reference.
import { useCallback, useSyncExternalStore } from "react";
export default function useMediaQuery(mediaQueryString: string) {
const subscribe = useCallback(
(callback: () => void) => {
const mediaQueryList = window.matchMedia(mediaQueryString);
mediaQueryList.addEventListener("change", callback);
return () => {
mediaQueryList.removeEventListener("change", callback);
};
},
[mediaQueryString],
);
return useSyncExternalStore(
subscribe,
() => window.matchMedia(mediaQueryString).matches,
() => undefined,
);
}
Note that the subscribe function is wrapped by useCallback hook to prevent
from re-subscribing unnecessarily.
Conclusion
Admittedly, useMediaQuery hook can be written without useSyncExternalStore.
For example we can either write useMediaQuery using useEffect and useState
which are familiar with a majority of developers.
However, nowadays we're getting more opportunities to render React component on
the server or at the build time such as Next.js, Remix and Astro. Therefore,
returning both the client and the server snapshots explicitly from
useSyncExternalStore is more cleaner IMO.
Here is the demo repository.