ハイドレーションの前に読む
テーマのクラスのように、最初の描画より前に<html>へ反映したい値があります。useAppStateはハイドレーションのあとに動くので、それでは間に合いません。このページでは、localStorageやsessionStorageの状態を、インラインスクリプトで先に読む方法を説明します。
このページの内容
キーを手で書かない
よくあるのは、保存キーとJSONの形をスクリプトの文字列に直接書く方法です。しかしそれでは、キーや保存の形が変わった瞬間に、定義と食い違ってしまいます。
defineLocalStateとdefineSessionStateの定義は、そのどちらも持っています。storageKeyはストアが書き込むキーで、inlineRead()はインラインの<script>に埋め込むJavaScriptの式を返します。この式は、ブラウザで評価されると、その定義の置き場所に保存されたオブジェクトになります。
inlineRead()があるのは、localStorageとsessionStorageの定義だけです。Cookieの状態なら、リクエストを受け取るページでサーバーがparseCookiesで読み、最初の描画から本当の値で描けます。
inlineReadを埋め込む
inlineRead()が返すのは式なので、スクリプトの中で値として使います。
state.tsimport { defineLocalState } from '@k8ordo/state';
import * as z from 'zod';
export const themeState = defineLocalState(
'theme',
z.object({ mode: z.enum(['light', 'dark']).optional() }),
);routes/layout.tsximport type { ReactNode } from 'react';
import { themeState } from '../state';
const applyTheme = `(() => {
const s = ${themeState.inlineRead()};
if (s && s.mode === 'dark') {
document.documentElement.classList.add('dark');
}
})();`;
export default function RootLayout({
children,
}: {
children: ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script>{applyTheme}</script>
</head>
<body>{children}</body>
</html>
);
}スクリプトはReactより先に<html>のクラスを変えるので、<html>にはsuppressHydrationWarningを付けます。ハイドレーションのあとは、ストアの値を正として扱ってください。
式は即時実行の関数なので、代入の右辺でも、関数の引数でも、三項演算子の中でも使えます。キーは<も含めてスクリプトの中で安全な形にエスケープされるので、どんなキーでも埋め込めます。
nullになるとき
式は投げません。次のときは、nullになります。
- 何も保存されていない
- 保存されたJSONが壊れている
- 値がオブジェクトではない(数や文字列、配列、
null) - ストレージそのものが読めない
versionを持つlocalStorageの状態で、行がほかの版で書かれている
返ってくるのは生の行
スクリプトが動く時点では、まだどのモジュールも読み込まれていません。そのためスキーマは走らず、返ってくるのはフィールドごとに拾った状態ではなく、保存されていた生の行です。
中身を信頼せず、必要なフィールドだけを、それぞれ自分で確かめて読んでください。上の例がs && s.mode === 'dark'と書いているのは、そのためです。
メモ
@k8ordo/color-schemeは、このinlineRead()を使って、最初の描画の前に<html>へクラスを付けています。カラースキームを切り替えるなら、自分で書かずにこのパッケージを使ってください。