Jaanis
Jaanis

Reputation: 69

How do I use Media Queries in the Next.js App Router?

I am using Next.js 13 with the App Router and have the following client component, which uses media queries inside the javascript to display a sidebar differently for small/big screens.

"use client";

export default function Feed() {
    const [isLargeScreen, setIsLargeScreen] = useState(window.matchMedia("(min-width: 768px)").matches);

    useEffect(() => {
    window
        .matchMedia("(min-width: 1024px)")
        .addEventListener('change', e => setIsLargeScreen(e.matches));
    }, []);

    return (
        <div>
            <Sidebar isLargeScreen={isLargeScreen}/>
            <div>...</div>
        </div>
    )
}

Now, the site loads inside the client perfectly, but since the Next.js App Router renders this component once on the server and the server has no window property, I will always get this error on the server (the console running npm run dev in local development mode):

error ReferenceError: window is not defined
at Feed (./app/feed/page.tsx:32:95)
> 17 |     const [isLargeScreen, setIsLargeScreen] = useState(window.matchMedia("(min-width: 768px)").matches);

I can replace the troublesome line with a if-else like this:

const [isLargeScreen, setIsLargeScreen] = useState(typeof window == "undefined" ? true : window.matchMedia("(min-width: 768px)").matches);

which then results in an runtime error on the client, if the server renders the component with the state set to true, but the client (on a small screen in this example) renders the component with the state set to false:

Unhandled Runtime Error

Error: Hydration failed because the initial UI does not match what was rendered on the server.

How can change this component so the server and client will not throw any errors?

Upvotes: 1

Views: 6215

Answers (5)

pacoyass
pacoyass

Reputation: 31

can you try this return false or true to sidebar:


import { useState, useEffect } from 'react';
import Sidebar from "./components/Sidebar";

export default function Home() {
  const [isLargeScreen, setIsLargeScreen] = useState(false);

  useEffect(() => {
    
      const mediaQuery = window.matchMedia("(min-width: 768px)");
      const handleChange = (e) => setIsLargeScreen(e.matches);

    
      setIsLargeScreen(mediaQuery.matches);

      mediaQuery.addEventListener('change', handleChange);

      
      return () => {
          mediaQuery.removeEventListener('change', handleChange);
      };
  }, []);
  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
     <h1>home</h1>
     <Sidebar isLargeScreen={isLargeScreen} />
    </main>
  );
}```

Upvotes: 0

pacoyass
pacoyass

Reputation: 31

hooks/use-media-query.js

import React, { useState, useEffect } from "react";

export function useMediaQuery(query) {
  const [value, setValue] = useState(false);

  useEffect(() => {
    function onChange(event) {
      setValue(event.matches);
    }
    const result = window.matchMedia(query);
    result.addEventListener("change", onChange);
    setValue(result.matches);
    return () => result.removeEventListener("change", onChange);
  }, [query]);

  return value;
}```
lib/mediaQueries.js
```import { useMediaQuery } from "@/hooks/use-media-query";

export const useDeviceSizes = () => {
  const isSmallMobile = useMediaQuery("(max-width: 359px)"); // Smaller than iPhone SE
  const isMobile = useMediaQuery("(min-width: 360px) and (max-width: 539px)"); // Small to regular mobile phones
  const isSurfaceDuo = useMediaQuery("(min-width: 540px) and (max-width: 719px)"); // Surface Duo
  const isTablet = useMediaQuery("(min-width: 720px) and (max-width: 1023px)"); // Tablets and larger devices
  const isSmallDesktop = useMediaQuery("(min-width: 1024px) and (max-width: 1439px)"); // Small desktops and tablets like iPad Pro 12.9"
  const isLargeDesktop = useMediaQuery("(min-width: 1440px)"); // Large desktops and monitors

  return {
    isSmallMobile,
    isMobile,
    isSurfaceDuo,
    isTablet,
    isSmallDesktop,
    isLargeDesktop,
  };
};```
Usage in page
```"use client"
import { useDeviceSizes } from '@/lib/mediaQueries';
import React from 'react';

export default function Page() {
  const { isSmallMobile, isMobile, isTablet, isSurfaceDuo, isSmallDesktop, isLargeDesktop } = useDeviceSizes();

  return (
    <div className="flex items-center justify-center text-7xl py-16">
      {isSmallMobile && <p>Viewing on a very small mobile device</p>}
      {isMobile && <p>Viewing on a mobile device</p>}
      {isSurfaceDuo && <p>Viewing on a Surface Duo</p>}
      {isTablet && <p className='text-5xl'>Viewing on a tablet</p>}
      {isSmallDesktop && <p>Viewing on a small desktop or large tablet</p>}
      {isLargeDesktop && <p>Viewing on a large desktop</p>}
    </div>
  );
}

Upvotes: 1

pacoyass
pacoyass

Reputation: 31

import { useMediaQuery } from "@/hooks/use-media-query";

export const useDeviceSizes = () => {
  const isSmallMobile = useMediaQuery("(max-width: 359px)"); // Smaller than iPhone SE
  const isMobile = useMediaQuery("(min-width: 360px) and (max-width: 539px)"); // Small to regular mobile phones
  const isSurfaceDuo = useMediaQuery("(min-width: 540px) and (max-width: 719px)"); // Surface Duo
  const isTablet = useMediaQuery("(min-width: 720px) and (max-width: 1023px)"); // Tablets and larger devices
  const isSmallDesktop = useMediaQuery("(min-width: 1024px) and (max-width: 1439px)"); // Small desktops and tablets like iPad Pro 12.9"
  const isLargeDesktop = useMediaQuery("(min-width: 1440px)"); // Large desktops and monitors

  return {
    isSmallMobile,
    isMobile,
    isSurfaceDuo,
    isTablet,
    isSmallDesktop,
    isLargeDesktop
  };
};

Upvotes: 0

ShueiYang
ShueiYang

Reputation: 808

I think you can use a trick like a "lazy hydration" in Next.js, there is severals methods, for example:

You can create a custom Hook, by creating a new file (useIsLargeScreen?)in the hooks folder :

 function useIsLargeScreen() {

  const [isLargeScreen, setIsLargeScreen] = useState(false); 

  useEffect(() => {
    setIsLargeScreen(window.matchMedia("(min-width: 768px)").matches);

    // I write this into a function for better visibility
    const handleResize = (e) => {
      setIsLargeScreen(e.matches);
    };

    const mediaQuery = window.matchMedia("(min-width: 1024px)");

    mediaQuery.addEventListener('change', handleResize);

    // Clean up the event listener when the component unmounts
    return () => {
      mediaQuery.removeEventListener('change', handleResize);
    };
  }, []);

  return {
    isLargeScreen
  }
};

export default useIsLargeScreen;

Than you use this hook on your Feed component:

export default function Feed() {
  // import this hook into this component
  const {isLargeScreen} = useIsLargeScreen();

  // maybe without conditional check if you want to render this on smaller screen with different style 
  return (
    <div>
      {isLargeScreen && <Sidebar isLargeScreen={isLargeScreen} />}
      <div>...</div>
    </div>
  );
}

Another method I think about would be import dynamically your Sidebar, like this:

    import dynamic from "next/dynamic";
    
    const Sidebar = dynamic(()=> import("../path/to/Sidebar"), {  //put your Sidebar component path
      ssr: false,
    })

    export default function Feed() {
    // your code...

    return (
      <div>
        <Sidebar isLargeScreen={isLargeScreen} />
        <div>...</div>
      </div>
    );
  }

there is also 3rd generic method to fix hydration error:

    export default function Feed() {

      const [ isMount, setIsMount ] = useState(false)

      useEffect(() => {
        setIsMount(true)
      }, []);

    // your code...


     return isMount ? (
       <div>
         <Sidebar isLargeScreen={isLargeScreen} />
         <div>...</div>
       </div>
     ) : <div />
   }

Upvotes: 3

grekier
grekier

Reputation: 3696

I've seen that it sometimes takes time to get the window object in client components and usually do a recursive check.

"use client";

export default function Feed() {
  const [isLargeScreen, setIsLargeScreen] = useState(false) //can't use window here

  const addListener = () => {
    if (window) {
      // Do whatever you need with window here...
      window
        .matchMedia("(min-width: 1024px)")
        .addEventListener('change', e => setIsLargeScreen(e.matches))
    } else {
      setTimeout(addListener, 100)
    }
  }

  useEffect(() => {
    addListener()
  }, [])

  return (
    <div>
      <Sidebar isLargeScreen={isLargeScreen}/>
      <div>...</div>
    </div>
  )
}

Upvotes: 1

Related Questions