React で localStorage 利用時の hydration エラーに対応する
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 が存在せず storageText
は undefined
になります。
一方でブラウザでの実行時には localStorage
から KEY
の値を読み出した文字列が storageText
に入り、レンダリング結果が不一致となりエラーが発生します。
このエラーに対して two-pass rendering という方法が提示されていますが、これは hydration 後に state 変更によるレンダリングを発生させるため、ややパフォーマンスが良くありません。
- https://reactjs.org/docs/react-dom.html#hydrate
- https://nextjs.org/docs/messages/react-hydration-error
useSyncExternalStore
useSyncExternalStore
は React 外のデータソースから読み込みを行うときに利用できる hook です。
以下のサンプルのように 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 中に返す値を指定することが出来ます。
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 からの値の読み出しを実装できます。
デモ
Discussion