useMedia

Sep 30, 2022·3 min read

The useMedia hook provides a convenient approach for working with media queries. This implementation was taken from the-platform by Jared Palmer.

import { useMedia } from '@ededejr/hooks';

function Example() {
	const small = useMedia('(min-width: 400px)');
	const medium = useMedia({ minWidth: 800 });
}

The returned values are stateful and will trigger a re-render if the media query changed. The function definition for the hook is as follows:

interface MediaQueryJSON {
	[key: string]: any;
}

function useMedia(
	query: string | MediaQueryJSON,
	initialMatch?: boolean
): boolean {}

Internally, useMedia registers an event listener on the result of window.matchMedia, and updates its state when the listener is triggered. Stepping outside of React for a moment, this implementation would resemble the following:

let doesQueryMatch = false;

const mediaQueryList = window.matchMedia(query);

mediaQueryList.addListener(() => {
	doesQueryMatch = mediaQueryList.matches;
});

With the core functionality illustrated above, we can now abstract this away behind the hook:

type MediaQuery = string | MediaQueryJSON;

function useMedia(query: MediaQuery, initialMatch = true) {
	const [matches, setMatches] = useState(initialMatch);

	useEffect(() => {
		const mediaQueryList = window.matchMedia(query);

		const listener = () => {
			if (mediaQueryList.matches) {
				setMatches(true);
			} else {
				setMatches(false);
			}
		};

		mediaQueryList.addListener(listener);
		setMatches(mediaQueryList.matches);

		return () => {
			mediaQueryList.removeListener(listener);
		};
	}, [query]);

	return matches;
}

While this captures most of what we want, there's a few more things we have to prevent some subtle bugs and make it a bit more resilient.

  1. Add a check to make sure window is defined, in case this hook is called on the server.
  2. In the previous example, we pass the MediaQuery type to window.matchMedia, but this function only accepts a string. To remedy this install json2mq, which is a handy utility that will convert our MediaQueryJSON into a string.
  3. listener could be called as soon as its registered, before the hook has had a chance to complete its first pass, or before teardown is complete. We can circumvent this by explicitly tracking when the listener should be active.

With all that completed, the final implementation is as follows:

import { useEffect, useState } from 'react';
import json2mq from 'json2mq';

type MediaQuery = string | MediaQueryJSON;

function useMedia(query: MediaQuery, initialMatch = true) {
	const [matches, setMatches] = useState(initialMatch);

	useEffect(() => {
		if (typeof window === 'undefined') {
			// 1
			return;
		}

		const mediaQueryList = window.matchMedia(
			typeof query === 'string' ? query : json2mq(query) // 2
		);

		let active = true; // 3

		const listener = () => {
			if (!active) {
				// 3
				return;
			}

			if (mediaQueryList.matches) {
				setMatches(true);
			} else {
				setMatches(false);
			}
		};

		mediaQueryList.addListener(listener);
		setMatches(mediaQueryList.matches);

		return () => {
			active = false; // 3
			mediaQueryList.removeListener(listener);
		};
	}, [query]);

	return matches;
}