skip to Main Content

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?

2

Answers


  1. 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>
        );
      }
    
    Login or Signup to reply.
  2. 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>
      )
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search