Search code examples
reactjsnext.jsnext.js13

Next.js 13 "window is not defined"


I wrote a component that looks like this:

'use client'

export const HorizontalModule = (props: any) => {
    ...

    return (
        {scrollPosition >= 0 && (
            <FirstModule />
          )}

          {scrollPosition >= window.innerHeight * 2 && (
            <SecondModule />
          )}        
    )
})

But I got the "window is not defined" error.

Reading through different posts, I have found that most people found using dynamic importing useful so I did this in the parent component which is a nextjs page:

const HorizontalModule = dynamic<any>(
  () => import('./HorizontalModule/HorizontalModule').then((mod) => mod.HorizontalModule),
  {
    ssr: false,
    suspense: true,
    loading: () => <p>Loading...</p>
  }
)

At first I was getting this error: "Object is not a function"

Now I'm getting "Unsupported Server Component type: undefined"

I don't exactly know what I did to switch the error but it is still not working.

I gotta mention, I use the window object all over the HorizontalModule code, in different useEffects but when I use it inside the render function, all stops working.

I also tried writing inside the component a validation like this:

if (window === undefined) return (<></>)
return (...)

I got the same window undefined object or a hydration error.

I don't know what else is there to do, ssr false doesn't work, suspense either, window condition...


Solution

  • From the Next.js 13 docs: https://beta.nextjs.org/docs/rendering/server-and-client-components#client-components

    [Client Components] are prerendered on the server and hydrated on the client.

    So the 'use client' directive does not render the page entirely on the client. It will still execute the component code on the server just like in Next.js 12 and under. You need to take that into account when using something like window which is not available on the server.

    You can't just check if the window is defined and then immediately update on the client either, since that may cause a mismatch between the server prerender and the client initial render (aka. a hydration error).

    To update the page on client load, you need to use a useEffect hook combined with a useState hook. Since useEffect is executed during the initial render, the state updates don't take effect until the next render. Hence, the first render matches the prerender - no hydration errors. More info here: https://nextjs.org/docs/messages/react-hydration-error

    Instead of creating this mechanism in every component that needs it, you can make a context that simply sets a boolean using useEffect, telling us we are safe to execute client code.

    is-client-ctx.jsx

    const IsClientCtx = createContext(false);
    
    export const IsClientCtxProvider = ({ children }) => {
      const [isClient, setIsClient] = useState(false);
      useEffect(() => setIsClient(true), []);
      return (
        <IsClientCtx.Provider value={isClient}>{children}</IsClientCtx.Provider>
      );
    };
    
    export function useIsClient() {
      return useContext(IsClientCtx);
    }
    

    _app.jsx

    function MyApp({ Component, pageProps }) {
      return (
        <IsClientCtxProvider>
          <Component {...pageProps} />
        </IsClientCtxProvider>
      );
    }
    

    Usage

      const isClient = useIsClient();
      return (
        <>
          {scrollPosition >= 0 && <FirstModule />}
    
          {isClient && scrollPosition >= window.innerHeight * 2 && <SecondModule />}
        </>
      );
    

    Live Demo: https://stackblitz.com/edit/nextjs-mekkqj?file=pages/index.tsx