端末に好みを保存する
表示形式や1ページの件数のような好みは、別のページに移っても、次に開いたときにも残っていてほしいものです。ブラウザの中に状態を置く定義は3つあり、残り方が違います。このページでは、defineLocalStateとdefineSessionState、defineMemoryStateの使い分けを説明します。
このページの内容
localStorageに置く
defineLocalStateは、状態をlocalStorageに置きます。消すまで残り、同じ端末のすべてのタブに同じ値が見えます。
prefs.tsimport { defineLocalState } from '@k8ordo/state';
import * as z from 'zod';
export const prefs = defineLocalState(
'prefs',
z.object({
view: z.enum(['grid', 'table']).default('grid'),
pageSize: z.number().default(20),
}),
);view-switch.tsxconst [{ view }, update] = useAppState(prefs, ['view']);
update({ view: 'table' });値はk8ordo-state:prefsというキーの1行に、スキーマに書いたフィールドだけのJSONとして保存されます。たとえば{"view":"table","pageSize":20}のような行です。このキーは、定義のstorageKeyで読めます。
JSONを通して保存するので、フィールドはJSONで表せる型にします。z.date()の値は、書いた直後には表示されますが、次に読み込んだときには既定値に戻ります。またentryと同じく、スキーマは自分が出した値を受け付けなければなりません。真偽値はz.stringbool()ではなくz.boolean()で書きます。
サーバーにはlocalStorageが無いので、サーバーの描画とハイドレーションの描画は既定値で行われ、そのあとで保存した値に切り替わります。サーバーの描画から好みを出したいなら、「サーバーが読む設定をCookieに置く」を見てください。最初の描画より前に<html>へ反映したいなら、「ハイドレーションの前に読む」を見てください。
ほかのタブと値をそろえる
localStorageの状態は、同じ端末で開いているほかのタブにも届きます。ほかのタブが書き込むとブラウザがstorageイベントを出し、ストアがその行を読み直すからです。
読み直したときに再描画されるのは、変わったキーを購読しているコンポーネントだけです。ほかのタブがpageSizeだけを変えたなら、['view']だけを購読しているコンポーネントは描き直されません。
タブを閉じるまでの値をsessionStorageに置く
defineSessionStateはdefineLocalStateと同じ作りで、置き場所だけがsessionStorageに変わります。
notices.tsimport { defineSessionState } from '@k8ordo/state';
import * as z from 'zod';
export const notices = defineSessionState(
'notices',
z.object({ dismissed: z.array(z.string()).default([]) }),
);再読み込みでは残り、タブを閉じると消えます。ほかのタブとは共有しないので、storageイベントが届くのは同じタブの中のほかのフレームだけです。
保存の形と読み方はlocalStorageと同じで、storageKeyもinlineRead()もあります。ただしversionとmigrateは取りません。行はタブと一緒に消えるので、デプロイをまたいで開いていたタブの行も、フィールドごとに拾えば足りるからです。
種類が違えば、キーが同じでも別の状態です。defineLocalStateとdefineSessionStateに同じ'prefs'を付けても、行も値も共有しません。
再読み込みで消えてよい値をメモリに置く
defineMemoryStateは、JavaScriptの実行環境に置く、型の付いた共有の箱です。離れたコンポーネントどうしで値を分け合い、再読み込みで初期値に戻ります。
palette.tsimport { defineMemoryState } from '@k8ordo/state';
export const palette = defineMemoryState('command-palette', {
open: false,
query: '',
});
type Panel = { tab: 'logs' | 'network' };
export const panel = defineMemoryState<Panel>('debug-panel', {
tab: 'logs',
});スキーマはありません。値が実行環境の外へ出ることがなく、型の付いたupdate()だけが書き手なので、確かめ直すものが無いからです。型は初期値から推論されます。ユニオン型のように初期値から推論できない型は、型引数で書きます。
update()はまとめられず、呼ぶたびにその場で反映されます。サーバーの描画は初期値で行われます。
落とし穴
値は書き換えずに、update()で置き換えてください。変わったかどうかは、update()で受け取った値を前の値と比べて決めます。そのため、入れ子のオブジェクトをその場で書き換えても、どのコンポーネントにも知らされません。
どれが残るか試す
localStorageとsessionStorage、メモリに1つずつ数を置いた、本物の定義です。3つとも同じ形で、違うのは置き場所だけです。
- localStorage
k8ordo-state:storage-demo-local0 - sessionStorage
k8ordo-state:storage-demo-session0 - メモリ保存しない0
試してみる
- 3つの数をそれぞれ何度か増やしてから、ページを再読み込みします。localStorageとsessionStorageの数は残り、メモリの数は0に戻ります。
- アドレスバーのURLをコピーし、新しいタブに貼り付けて開きます。引き継がれるのはlocalStorageの数だけで、sessionStorageの数は0から始まります。
- 新しいタブでlocalStorageの数を増やしてから、元のタブに戻ります。元のタブの数も、同じ値に変わっています。