javascriptreactjsnext.jsserver-side-renderingapp-router

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?


Solution

  • 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 />
       }