最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How do I use Media Queries in the Next.js App Router? - Stack Overflow

programmeradmin1浏览0评论

I am using Next.js 13 with the App Router and have the following client ponent, 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 ponent 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 ponent with the state set to true, but the client (on a small screen in this example) renders the ponent 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 ponent so the server and client will not throw any errors?

I am using Next.js 13 with the App Router and have the following client ponent, 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 ponent 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 ponent with the state set to true, but the client (on a small screen in this example) renders the ponent 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 ponent so the server and client will not throw any errors?

Share Improve this question asked Jul 20, 2023 at 18:55 JaanisJaanis 693 silver badges8 bronze badges 1
  • why you are using useEffect hook instead of try to write css classes in @media() {} and then import as module in your ponent – Kannu Mandora Commented Jul 23, 2023 at 13:10
Add a ment  | 

5 Answers 5

Reset to default 3

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 ponent unmounts
    return () => {
      mediaQuery.removeEventListener('change', handleResize);
    };
  }, []);

  return {
    isLargeScreen
  }
};

export default useIsLargeScreen;

Than you use this hook on your Feed ponent:

export default function Feed() {
  // import this hook into this ponent
  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 ponent 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 />
   }

I've seen that it sometimes takes time to get the window object in client ponents 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>
  )
}

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>
  );
}
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
  };
};

can you try this return false or true to sidebar:


import { useState, useEffect } from 'react';
import Sidebar from "./ponents/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>
  );
}```
发布评论

评论列表(0)

  1. 暂无评论