Next.jsのlocalStorage hydrationはinitializeWithValue falseだけでは足りない
Next.jsでは、localStorage はclient側にしかありません。server render時には読めません。
そのため、localStorage 由来の値を初期renderで使うと、serverのHTMLとclient初回renderがズレてhydration errorになることがあります。
usehooks-ts の useLocalStorage には、この問題を避けるための設定があります。
const [tabs, setTabs] = useLocalStorage("tabs", [], {
initializeWithValue: false,
});
ただし、これだけで全て解決ではありません。初回renderの不一致は避けられても、effectがhydration前のdefault値を読んで動くことがあります。
ありがちなバグ
次のコードは自然に見えます。
const [tabs, setTabs] = useLocalStorage<Tab[]>("tabs", [], {
initializeWithValue: false,
});
useEffect(() => {
if (tabs.length === 0) {
setTabs([createDefaultTab()]);
}
}, []);
しかし、useEffect が最初に動く時点では、tabs はまだ localStorage の値ではなくdefaultの [] です。
保存済みのtabsがある場合でも、hydration完了前にdefault tabを作ってしまうことがあります。
hydration guardを入れる
最初のeffect実行をskipし、依存配列に保存値を入れます。
const [tabs, setTabs] = useLocalStorage<Tab[]>("tabs", [], {
initializeWithValue: false,
});
const hydrated = useRef(false);
useEffect(() => {
if (!hydrated.current) {
hydrated.current = true;
return;
}
if (tabs.length === 0) {
setTabs([createDefaultTab()]);
}
}, [tabs, setTabs]);
これで、hookが保存値を読み直した後にeffectが反応できます。
undefined defaultを避ける
配列やobjectには、undefined ではなく型付きの空値を使います。
// 避ける
useLocalStorage<Tab[] | undefined>("tabs", undefined, {
initializeWithValue: false,
});
// 推奨
useLocalStorage<Tab[]>("tabs", [], {
initializeWithValue: false,
});
render側の分岐も単純になります。保存値から副作用を作るeffectがある場合は、hydration guardと組み合わせます。
initializeWithValue falseを使わない方がよい場合
server側リソースへ再接続するIDには注意が必要です。
- WebSocket session ID
- PTY session ID
- room ID
これらは初回mount時にすぐ必要なことがあります。default値で一瞬renderすると、別のserver-side resourceへ接続してしまう可能性があります。
まとめ
initializeWithValue: false は、UI preferenceや単純なclient stateには便利です。
ただし、その値を読んで副作用を起こすeffectがあるなら、hydration guardを入れてください。そうしないと、hydration前のdefault値に反応して、重複データや古い状態を作ることがあります。