エラー: 初期のUIとサーバーでレンダリングされた内容が一致しないため、ハイドレーションに失敗しました

エラー: 初期のUIとサーバーでレンダリングされた内容が一致しないため、ハイドレーションに失敗したカバー画像

修正に移動する

バグを遭遇した場所

export default function Nav() {
  const user = getUser();

  return (
    {user ? <AuthenticatedNav /> : <UnauthenticatedNav />} // この記述でエラーが発生
  );
}

ブラウザウィンドウに表示されるエラー:初期のUIとサーバーでレンダリングされた内容が一致しないため、ハイドレーションに失敗しましたというエラー
このエラー通知は問題のあるコードにつながる情報を提供しませんが、コンソールは提供します。
上記画像の詳細版

でも、なぜ?

ジャッキー・チェンの困惑GIF

このエラーの原因は?

アプリケーションをレンダリングしている間に、事前にレンダリングされた(SSR/SSG)Reactツリーと、ブラウザでの最初のレンダリング時にレンダリングされたReactツリーの間に差異がありました。最初のレンダリングはハイドレーションと呼ばれるReactの機能です。
これによりReactツリーがDOMと同期せず、意図しないコンテンツや属性が存在する可能性があります。 -nextjsドキュメント

うん、なんとなくわかるけど、でもなぜ?

やりたいことはこれだけ

ユーザーがログインしていれば<AuthenticatedNav>コンポーネントをレンダリングし、そうでなければ<UnauthenticatedNav>コンポーネントをレンダリングする。

「ハイドレーション」とは何か、なぜ使用されるのかを理解しよう

サーバーサイドレンダリング(SSR)は、nextjsなどのフレームワークが、パフォーマンス(LCP&FCP)とユーザーエクスペリエンス(SEO)を向上させるために使用し、アプリを最初にサーバー上でレンダリングします。これは完全に形成されたHTMLドキュメントをユーザーに返しますが、アプリは「動的」であり、すべてがHTML&CSSで達成できるわけではありません。したがって、アプリをインタラクティブにするために、ReactにHTMLへのイベントハンドラーを添付するよう指示します。
コンポーネントをレンダリングし、イベントハンドラーを添付するこのプロセスを「ハイドレーション」と呼びます。これは、「乾燥した」HTMLに「相互作用性(JS)の水」を注ぐようなものです。ハイドレーション後、アプリケーションはインタラクティブまたは「動的」になります。
ハイドレーションと再ハイドレーションはしばしば同義で使用されますが、再ハイドレーションでは、クライアントサイドのJSはコンパイル時に生成されたのと同じReactコードを含みます。
このコードはユーザーのデバイス上で実行され、世界がどのように見えるべきかの画像を構築します。その後、それをドキュメントに組み込まれたHTMLと比較します。これは再ハイドレーションと呼ばれるプロセスです。

再ハイドレーションでは、ReactはDOMが変更されないと想定しています。それは、既存のDOMに適応しようとしています。
Reactアプリが再ハイドレーションを行うとき、DOM構造が一致することを前提としています。一致しない場合は、ご存知の通りです。
コンソールエラー:初期のUIとサーバーでレンダリングされた内容が不一致のためハイドレーションに失敗した
ですので、私たちの場合、サーバー上ではuser状態を知ることができず、クライアントにマウントされるまでuser状態はundefinedを返します。

では、この問題をどう解決するか

基本的には、以下の2つの方法で修正できます。

  1. useEffect / useMountedフック
  2. コンポーネントでラッピングする

useEffectuseMountedカスタムフック

export default function Nav() {
  const [hasMounted, setHasMounted] = React.useState(false);
  React.useEffect(() => {
    setHasMounted(true);
  }, []);
  if (!hasMounted) {
    return null;
  }
  const user = getUser();
  return (
    {user ? <AuthenticatedNav /> : <UnauthenticatedNav />}
  );
}

または

useHasMountedカスタムフック

function useHasMounted() {
  const [hasMounted, setHasMounted] = React.useState(false);
  React.useEffect(() => {
    setHasMounted(true);
  }, []);
  return hasMounted;
}
コンポーネントでラッピングする

<clientOnly>コンポーネント

function ClientOnly({ children, ...delegated }) {
  const [hasMounted, setHasMounted] = React.useState(false);
  React.useEffect(() => {
    setHasMounted(true);
  }, []);
  if (!hasMounted) {
    return null;
  }
  return (
    <div {...delegated}>
      {children}
    </div>
  );
}

その後、クライアントサイドでのみレンダリングすることを望むコンポーネントをラッピングできます。

<ClientOnly>
  <Nav/>
</ClientOnly>
僕がしたこと

nav.js

import { UserState } from '../../context/userProvider'

export default function Nav() {
  return (
    UserState() ? <AuthenticatedNav /> : <UnauthenticatedNav />
  );
}

userProvider.js //グローバルユーザーコンテキストプロバイダーを使用

export function UserState() {
  const { user } = useUser();
  const [isUserLoggedIn, setIsUserLoggedIn] = useState(false);
  useEffect(() => {
    setIsUserLoggedIn(!!user);
  }, [user]);
  return isUserLoggedIn;
}

🙏読んでくださってありがとうございます。この記事が参考になれば幸いです。批判、提案、または訂正があれば、コメントセクションでお知らせください。

次回まで

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/rkjain119/error-hydration-failed-because-the-initial-ui-does-not-match-what-was-rendered-on-the-server-1d0f