8

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...

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
H3lltronik
  • 562
  • 8
  • 26

2 Answers2

17

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

Chris Hamilton
  • 9,252
  • 1
  • 9
  • 26
  • Just for posterity, my first working solution was to directly add the window validation right at the start of the component so no code was run if there were no window but it logs a lot of errors to vscode. Using you approach I surrounded my HorizontalModule with IsClientProvider, used the hook normally and only had validate if it was true inside my useEffects and the render. Thank you – H3lltronik Mar 10 '23 at 15:19
  • Nice work, such a shame next13 doesn't work.... – Ben Winding May 25 '23 at 13:08
  • Getting `ReferenceError: createContext is not defined`. This worked for me instead: https://stackoverflow.com/questions/75369036/window-is-not-defined-next-js-13-client-component-in-server-component – Nick Jun 23 '23 at 14:35
  • @Nick `import { createContext } from 'react'`. You can check out the live demo for an example. – Chris Hamilton Jun 23 '23 at 15:27
  • @Nick although that other answer is useful, it completely disables ssr for that component, ie. the entire component is rendered on the client. This approach lets you render everything you can on the server, then perform updates on the client given a condition. – Chris Hamilton Jun 23 '23 at 15:35
  • Thanks @ChrisHamilton in my case it's an audio player component so without SSR is fine. Sorry I missed the demo, I'll try again later with another component. – Nick Jun 24 '23 at 03:05
-1
'use client'

import { useEffect, useState } from 'react'

type IUseStickyHeaderProps = {
   active: boolean
}  

export const useStickyHeader = ():IUseStickyHeaderProps => {
    const [active, setActive] = useState(false)

    useEffect(() => {
        window.addEventListener('scroll', () => {
            if (window.scrollY > 70) {
                setActive(true)
            } else {
                setActive(false)
            }
        })
    }, [])

    return {
        active
    }
}