3

I have a common problem: My header should show "register" for guests or "profile" for users.

The header is in the root layout file, the user state is saved in a cookie.

In a client component, I am reading the cookie and in useEffect change the status. A simplified version will be this:

'use client';


export default function NavRegisterOrProfile() {
  const [user, setUser] = useState(false);

  useEffect(() => {
    const user = getUserCookie();
    if (user) {
      setUser(user);
    }
  }, []);


  return (
    <>
      {user ? <div>Profile</div> : <div>Register</div> }
    </>


Problem: This creates a hydration error for users (for guests I am not getting an error):

Error: Hydration failed because the initial UI does not match what was rendered on the server

Undesired solution: I can read the cookie on the server side using

import { cookies } from 'next/headers';

export default function Page() {
  const nextCookies = cookies();
  const theme = nextCookies.get('user');
  return '...'
}

But then my entire site (since this is in the root layout) cannot be static site generated (SSG) and moves to SSR. So this is a big performance issue.

Another undesired solution: I can show nothing until the component is mounted, but for SEO reasons I don't want the UI to shift creating a CLS issue. I want the guest version to be the default SSG, and after the component mounts - change to the user version without the hydration issue.

Full actual code:

'use client';

import React, { Suspense, useEffect, useState } from 'react';
import { hookstate, useHookstate } from '@hookstate/core';
import { getUserCookie } from 'helpers/cookieHelper';
import RegisterModal from '../RegisterModal';
import Link from 'next/link';
import UserAvatar from '@shared/user/UserAvatar';
import Image from 'next/image';

export const globalUserState = hookstate(false);

export default function NavRegisterOrProfile() {
  const [showRegister, setShowRegister] = useState(false);
  const [hasMounted, setHasMounted] = React.useState(false);

  const userState = useHookstate(globalUserState);
  useEffect(() => {
    setHasMounted(true);
    const user = getUserCookie(true);
    if (user) {
      userState.set(() => user);
    }
  }, []);

  const user = userState.get();

  return (
    <>
      {hasMounted && user?.name ? (
        <>
          <Link href="/profile" id="desktop-nav-profile">
            <UserAvatar user={user} />
          </Link>
          {user?.isAdmin && (
            <div className="shadow-md h-12 w-12 ml-4 flex justify-center items-center rounded-full">
              <Link href="/admin">
                <Image
                  src="/images/heart.svg"
                  width={30}
                  height={26}
                  alt="admin"
                  priority
                />
              </Link>
            </div>
          )}
        </>
      ) : (
        <>
          <strong
            className="mx-3 px-1 cursor-pointer hover:underline"
            data-cy="header-register"
            id="desktop-nav-register"
            onClick={() => setShowRegister(true)}
          >
            Join Us
          </strong>

          {showRegister && (
            <Suspense>
              <RegisterModal
                closeModalCallback={() => {
                  setShowRegister(false);
                }}
              />
            </Suspense>
          )}
        </>
      )}
    </>
  );
}

Tomer Almog
  • 3,604
  • 3
  • 30
  • 36
  • 2
    I'm not able to reproduce the issue. Aside from the `user` state having `false` as default value and `getUserCookie` probably not returning a boolean, I got two questions: are "Profile" and "Register" in the simplified example supposed to be components? and if so, are they client components as well? – ivanatias Nov 27 '22 at 23:03
  • They can be components, but they don't have to. In my code, the profile shows an avatar image of the user. I do return false from `getUserCookie` if it is not present. – Tomer Almog Nov 27 '22 at 23:54
  • 1
    By any chance, are you using MUI or styled-components in your project? – ivanatias Nov 28 '22 at 00:01
  • Mui yes, but no styled-components. – Tomer Almog Nov 28 '22 at 01:07
  • 2
    Can you include in your question the exact code for "Profile" and "Register"? I suspect the issue is an incorrect link wrapping, for example, wrapping a MUI `Link` component with a Next.js `Link` component, since I imagine the user's avatar for example, links to the user's profile or something. – ivanatias Nov 28 '22 at 01:14
  • Included @ivanatias – Tomer Almog Nov 28 '22 at 01:19
  • to be clear - the error shows only when user exists – Tomer Almog Nov 28 '22 at 01:24
  • For users, I am getting an error even with this return statement: `return <>{user ?
    profile
    :
    register
    }>;`
    – Tomer Almog Nov 28 '22 at 01:26

1 Answers1

0

So I found an ugly solution, I really don't want to use it, but it makes the annoying error go away...

Adding timeout on the useEffect "solves" this since the initial render matches the server DOM.

useEffect(() => {
    setHasMounted(true);
    const user = getUserCookie(true);
    if (user) {
      setTimeout(() => {
        userState.set(() => user);
      }, 100);
    }
  }, []);

Let's agree as a community that this trash can't be the accepted answer here. Anyone has a better solution?

Tomer Almog
  • 3,604
  • 3
  • 30
  • 36