Search code examples
reactjsnext.jsserver-side-renderingclient-sidenext.js13

Client and server-side out differ when working with user who is logged in (hydration error)


I'm facing React's hydration error, which - according to the documentation - is caused by "using browser-only APIs like window or localStorage in your rendering logic".

This probably happening at all places where I'm checking whether the user is logged in (and often run GraphQL queries for user-specific elements to display on the client-side), b/c I'm using cookies here (simplified):

import { useCookies } from 'react-cookie';

export const useSessionCookieData = (): Session => {
  const [cookieData] = useCookies(sessionKey);
  return cookieData.session.accessToken;
};

My core idea was that the page is rendered on the server side as if being logged out, and the client-side might change this in case the user is logged in. My question is which solution is preferable or whether there's even a better one than those:

  1. I could give up on server-side rendering. This is probably a loss in performance, but I'm not sure whether it's important on an heavily interactive side if many components depend on my login status anyway.
  2. I could prevent all components render differently depending on the login stage from being rendered at all. Those are plenty, and I'ld rather not go through all of them. Ideally, I could do something like:
export const useSessionCookieData = (): Session => {
  const [cookieData] = useCookies(sessionKey);
  return cookieData.session.accessToken;
};

Since useCookies calls useState internally (and additionally ensures whether it's in browser, but by "forbidden" window checks), I'ld expected it to automatically enforce client-side rendering, but - as the hydration error conveys - that doesn'twork. Is there any way I could make sure using useSessionCookieData enforces the calling component to client-side one only? Adding something like

const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (mounted) return undefined;

in my useSessionCookieData doesn't change anything, either, and additionally I don't want to return undefined initially (and trigger an undesired redirect to the login page in some circumstances).

3.) I could try to pass the access token to the server-side and let it do all the queries. This is probably rather hard and means quite a rewrite?

At the moment, I have the impression that my use case - a rather customized website - is not made for server-side rendering, and there probably isn't much gain from it. So I'm in favor of option 1.)

Are there any other ideas or recommendations how to deal with that problem?


Solution

  • Personally, I have a terrible experience with react-cookie that makes it work the React way -- for example, you can add dependency in the argument so when the cookie changes, it should rerender the component, but NOPE, it doesn't work that way.

    Anyway, there are a couple of problems I can observe --

    1. Getting cookies during SSR and Client-Side Rendering (CSR) are different. In SSR, you get that from context.
    export const getServerSideProps: GetServerSideProps = async (context) => {
        const cookie = context.req.cookies
        // ... 
    }
    

    whereas in CSR, you get cookie from window object like window.document.cookie or just document.cookie.

    So its probably one of the reason why your page didn't get hydrated properly -- the cookie is not properly parsed and validated.

    1. This is more of a personal preference -- during CSR, use universal-cookie instead to get/set cookies the traditional way. It is included in the react-cookie so you don't have to install another module.
    import { Cookies } from 'react-cookie'
    
    const cookies = new Cookies()
    
    // get a key of a cookie
    const someCookieValue = cookies.get('some-cookie-key')
    
    // set a key to a cookie
    cookies.set('some-cookie-key', 'some-value')