useMediaQuery

Check if the media query matches the current viewport

The current viewport is mobile

Installation

Copy-paste the hook

Copy and paste the hook code in a .ts file.

import { useState } from 'react';
 
import { useIsomorphicLayoutEffect } from '@kosori/hooks/use-isomorphic-layout-effect';
 
type Props = {
  defaultValue?: boolean;
  initializeWithValue?: boolean;
};
 
const IS_SERVER = typeof window === 'undefined';
 
/**
 * A custom hook that returns the current match status of a given media query.
 *
 * @param {string} [query] - The media query string to evaluate.
 * @param {Props} [options={}] - Configuration options for the hook.
 * @param {Props['defaultValue']} [options.defaultValue] - The default value used on the server.
 * @param {Props['initializeWithValue']} [options.initializeWithValue] - If `true`, the hook initializes with the media query's match status on the first render.
 *
 * @returns A boolean indicating whether the media query currently matches.
 *
 * @example
 * const isDesktop = useMediaQuery('(min-width: 768px)');
 * const isTablet = useMediaQuery('(max-width: 767px)');
 */
export const useMediaQuery = (
  query: string,
  { defaultValue = false, initializeWithValue = true }: Props = {},
) => {
  const getMatches = (query: string): boolean => {
    if (IS_SERVER) {
      return defaultValue;
    }
    return window.matchMedia(query).matches;
  };
 
  const [matches, setMatches] = useState<boolean>(() => {
    if (initializeWithValue) {
      return getMatches(query);
    }
    return defaultValue;
  });
 
  useIsomorphicLayoutEffect(() => {
    const matchMedia = window.matchMedia(query);
 
    const handleChange = () => setMatches(getMatches(query));
    handleChange();
 
    // Safari < 14 compatibility with addListener/removeListener
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (matchMedia.addListener) {
      matchMedia.addListener(handleChange);
    } else {
      matchMedia.addEventListener('change', handleChange);
    }
 
    return () => {
      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
      if (matchMedia.removeListener) {
        matchMedia.removeListener(handleChange);
      } else {
        matchMedia.removeEventListener('change', handleChange);
      }
    };
  }, [query]);
 
  return matches;
};

Usage

import { useMediaQuery } from '~/hooks/use-media-query';
useIsomorphicLayoutEffect(() => {
  console.log(
    "In the browser, I'm an `useLayoutEffect`, but in SSR, I'm an `useEffect`.",
  );
}, []);

Details

Server-Side Rendering (SSR) and Client-Side Behavior

  • The hook uses an isomorphic approach to handle both SSR and CSR (client-side rendering). When rendering on the server, useMediaQuery uses a default value (false by default) because window.matchMedia is not available during SSR.
  • On the client, the hook utilizes window.matchMedia to accurately determine whether the provided media query matches the current viewport.
  • The initializeWithValue option allows you to control whether the initial render on the client uses the actual match status or a fallback default value, which can help avoid UI mismatches between SSR and CSR.

Media Query Listener

  • The hook listens for changes to the viewport that affect the provided media query. Whenever the viewport crosses the query threshold (e.g., resizing from mobile to desktop), the hook updates the matches state accordingly.
  • It supports Safari < 14 compatibility by using both addListener/removeListener (deprecated) and addEventListener/removeEventListener for media query events.

Default Value and Initialization

  • defaultValue: This is primarily useful for SSR. Since window.matchMedia is not available on the server, the hook returns this default value during the server-side rendering phase.
    • Example: You may set defaultValue to true if you want the server-rendered version to assume a "mobile-first" approach when the screen size is unknown.
  • initializeWithValue: This flag controls whether the hook immediately checks the current media query match on the first render. If set to false, it skips this check and uses defaultValue until the next effect cycle.

Return Value

  • The hook returns a simple boolean (true or false) indicating whether the media query matches the current viewport. This value can be directly used to conditionally render elements or apply logic in the component.

On this page