Next.jsのHydration Errorに、useEffect以外で3つのやり方で回避する

Next.jsで開発しているとよく遭遇するあのエラー。

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

Hydration Errorです。サーバー側のレンダリング結果とクライアント側の結果がズレたときに出るやつです。

とりあえずuseEffectという力技はやめて、もうちょっとマシなやり方を試行錯誤した話です。


何が起きていたのか

自分の場合は「現在時刻のリアルタイム表示」とか「ダークモードの切り替え」とかのコンポーネントを作ったときでした。

サーバーでビルドされた時点の時間とか、ローカルストレージに保存された設定なんかが、ブラウザ側では違う値になってて、サーバーとクライアントで違うぞ!とReactに怒られたのです。

よくある「とりあえずuseEffect」

以下みたいにすると、一応手っ取り早く消せます。

import { useState, useEffect } from 'react';

export default function CurrentTime() {
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  if (!isMounted) return null;

  return <div>現在の時刻: {new Date().toLocaleTimeString()}</div>;
}

確かにエラーは消えてくれるんですが、気になる点はありました。

  • 無駄な再レンダリング(空で描画 → マウント後に本描画で2回走る)
  • 一瞬なんかチラつく
  • 毎回この定型文を書くのが面倒くさい

もっと良い方法がないか調べて、実際に導入したのが以下の3つです。


方法1:next/dynamic

一番手軽だったのが、最初からSSRしないって決めてしまうやり方です。
TimeDisplay.tsx みたいに別ファイルに切り出して、呼び出し側でこうします。

import dynamic from 'next/dynamic';

const TimeDisplayNoSSR = dynamic(
  () => import('@/components/TimeDisplay'),
  { ssr: false }
);

export default function HomePage() {
  return (
    <main>
      <h1>マイページ</h1>
      <TimeDisplayNoSSR />
    </main>
  );
}

コンポーネント側に変なロジックがいらないし、呼び出し側で {ssr: false}つけるだけ。SEOが関係ない部分(ログイン後のユーザー名とか、リアルタイム情報)は、最初からSSRの対象から外しちゃうのが楽でした。


方法2:React 18のuseSyncExternalStore

もう一つ試したのが、ブラウザのAPI(ウィンドウサイズとか)を扱うときの公式推奨フックです。

useSyncExternalStoreを使うと、コンポーネントと外部の値(ストア)を安全に同期できます。
ウィンドウサイズで表示を変える処理で実装してみました。

import { useSyncExternalStore } from 'react';

const subscribe = (callback: () => void) => {
  window.addEventListener('resize', callback);
  return () => window.removeEventListener('resize', callback);
};

const getClientSnapshot = () => window.innerWidth;
const getServerSnapshot = () => 1024;

export default function ResponsiveComponent() {
  const width = useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);

  return <div>現在の画面幅: {width}px</div>;
}

コードの量は増えるけど、チマチマuseEffect+useStateでやるより、リサイズしたときのカクつきが防げて、イベントのクリーンアップもきれいに書けます。

自作のカスタムフックとしてuseWindowSizeみたいなの作るとしたら、これで行くべきだなって思いました。


方法3:suppressHydrationWarning

どうしてもコンポーネント側で修正できない場合は、該当するタグにsuppressHydrationWarningを付けることで、Hydration Errorを無視することもできます。

return <div suppressHydrationWarning>{content}</div>;

あくまで警告を無視してるだけなので、ちょっとしたレイアウトの崩れとかもスルーされちゃう可能性があるので、ピンポイントで使うのが安全です。


使い分け

いろいろ試した結果、自分の中での基準はこんな感じです。

場面選択肢
非SEO付やユーザー情報の一部)だけSSRを除外したいnext/dynamic{ssr: false}
ウィンドウサイズやブラウザAPIと連動させたい(こだわりたいとき)next/dynamic+useSyncExternalStore
いち早く黙らせたい(最終手段)suppressHydrationWarning

状況によって適切な方法を選ぶと、コードも複雑にならなくて、画面のガタつきも防げられます。同じエラーで困ったときの参考になれば。