🗃️

React で localStorage 利用時の hydration エラーに対応する

2023/01/17に公開

Next.js の Static Generation/Server-side Rendering などの hydration が発生する環境で、以下のような localStorage から値を読み込む処理を作ると hydration エラーが発生します。

const Page: FC = () => {
  const storageText =
    typeof window === "undefined" ? "" : localStorage.getItem("KEY");

  return (
    <div>
      <h1>Hydration Error</h1>
      <p>{storageText}</p>
    </div>
  );
};

Static Generation/Server-side Rendering 等の Node.js による処理でコンポーネントが描画されるタイミングでは localStorage が存在せず storageTextundefined になります。
一方でブラウザでの実行時には localStorage から KEY の値を読み出した文字列が storageText に入り、レンダリング結果が不一致となりエラーが発生します。

このエラーに対して two-pass rendering という方法が提示されていますが、これは hydration 後に state 変更によるレンダリングを発生させるため、ややパフォーマンスが良くありません。

useSyncExternalStore

useSyncExternalStore は React 外のデータソースから読み込みを行うときに利用できる hook です。
https://reactjs.org/docs/hooks-reference.html#usesyncexternalstore
https://beta.reactjs.org/reference/react/useSyncExternalStore

以下のサンプルのように browser API の購読に利用する例が紹介されています。
https://beta.reactjs.org/reference/react/useSyncExternalStore#subscribing-to-a-browser-api

import { useSyncExternalStore } from 'react';

function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}

function getSnapshot() {
  return navigator.onLine;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

useSyncExternalStore を使った localStorage からの読み込み

この useSyncExternalStore ですが、実は3つ目の引数で server rendering/hydration 中に返す値を指定することが出来ます。
https://react.dev/reference/react/useSyncExternalStore#usesyncexternalstore

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

これを用いて localStorage からの値の読み出しを作ると以下のような hook を作成できます。

import { useSyncExternalStore } from "react";

export const STORAGE_TEXT_KEY = "STORAGE_TEXT";

export const useStorageText = () => {
  const storageText = useSyncExternalStore(
    subscribe,
    () => localStorage.getItem(STORAGE_TEXT_KEY),
    () => ""
  );

  return storageText;
};

const subscribe = (callback: () => void) => {
  window.addEventListener("storage", callback);

  return () => {
    window.removeEventListener("storage", callback);
  };
};

この hook を用いると hydration エラーを発生させずに localStorage からの値の読み出しを実装できます。

デモ

https://github.com/yami-beta/nextjs-localstorage-hydration-sample

Discussion