NextJs Testing: window.matchMedia is not a function in use-mobile hook

While writing tests for my Next.js app using Vitest and React Testing Library, I ran into an annoying error that looked like this:

TypeError: window.matchMedia is not a function
 ❯ hooks/use-mobile.ts:12:24

The error was pointing to this line inside my use-mobile.tsx hook:

const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);

This hook helps determine if the user is on a mobile device using matchMedia. It works perfectly in the browser—but crashes in the test environment because window.matchMedia isn’t defined in JSDOM (the environment Vitest uses for running tests).


🛠️ The Fix: Add a Defensive Check

The quick fix was to wrap the entire useEffect logic in a condition that checks if window and window.matchMedia exist. This avoids calling matchMedia during tests (or in non-browser environments like SSR).

❌ Before (Fails in Tests)

useEffect(() => {
  const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
  const onChange = () => {
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
  };
  mql.addEventListener("change", onChange);
  setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);

  return () => mql.removeEventListener("change", onChange);
}, []);

✅ After (Test-Safe)

useEffect(() => {
  if (typeof window !== "undefined" && window.matchMedia) {
    const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
    const onChange = () => {
      setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
    };
    mql.addEventListener("change", onChange);
    setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);

    return () => mql.removeEventListener("change", onChange);
  }
}, []);

This makes your hook resilient both in tests and during server-side rendering (SSR).


💡 Bonus Tip: Mock matchMedia in Your Test Setup

Alternatively (or additionally), you can mock matchMedia in your test setup file:

// setup.ts (or setupTests.ts)
beforeAll(() => {
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: (query) => ({
      matches: false,
      media: query,
      onchange: null,
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      dispatchEvent: vi.fn(),
    }),
  });
});

This ensures your tests don’t break even if matchMedia is called. But I still recommend the typeof window !== "undefined" check in your hook, just to be safe in SSR contexts.


🧵 Wrap-up

If you're working with matchMedia in a React hook, always remember that test environments and SSR might not have the window object or browser APIs like matchMedia. Adding a simple check avoids runtime errors and keeps your tests green. 🌱

If you found this helpful or ran into a similar issue, let me know! I'd love to hear how you handled it.

1
Subscribe to my newsletter

Read articles from Lateef Quadri Olayinka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Lateef Quadri Olayinka
Lateef Quadri Olayinka

I am AbdQaadir by name. A Nigeria based Front End Developer. I'm interested in Software Engineering and Cyber Security.