@k8ordo/state

履歴エントリに状態を置く

開いている行や広げたパネルのように、戻るボタンでは元に戻ってほしいけれど、リンクには載せたくない状態があります。こうした状態は、履歴エントリの隠れた面であるentryに置きます。

このページの内容

entryに書く

definePageStateにentryのスキーマを渡します。値はURLには出ず、Navigation APIが履歴エントリごとに持つ状態の中に、定義のキーを名前空間にして保存されます。

orders-state.ts
import { definePageState } from '@k8ordo/state';
import * as z from 'zod';

export const ordersState = definePageState('orders', {
  entry: z.object({
    expanded: z.array(z.string()).default([]),
    showNotes: z.boolean().default(false),
  }),
});

値は文字列にされず、型の付いたまま保存されます。そのため、真偽値はz.boolean()で、数はz.number()で、書いたとおりの型のまま受けます。

ただし、スキーマは自分が出した値をもう一度受け付けなければなりません。書いた値は、型の付いたままスキーマを通り直すからです。z.stringbool()や型を変える変換は、書くたびに既定値へ戻ってしまいます。

サーバーには履歴エントリが無いので、サーバーの描画とハイドレーションの描画は既定値で行われます。

urlとentryを1つの定義にまとめる

1つの定義に、urlとentryの両方を書けます。useAppStateが返す状態は、2つを平らにまとめたものです。

tsx
export const ordersState = definePageState('orders', {
  url: z.object({
    status: z.enum(['all', 'pending', 'shipped']).default('all'),
  }),
  entry: z.object({
    expanded: z.array(z.string()).default([]),
  }),
});

const [{ status, expanded }, update] = useAppState(ordersState);

フィールドをurlからentryへ移しても、変わるのは定義だけで、呼び出す側のコードはそのまま動きます。ただし綴りはフィールドについて回ります。urlでz.stringbool()だった真偽値は、entryではz.boolean()に書き換えます。

同じ名前のフィールドを両方に書くと、型エラーになります。実行時にも、"orders" declares in both url and entry: statusのように投げます。

2つの面は一度に書き込まれる

1回のupdate()で、urlとentryの両方を変えられます。2つの面は同じ履歴エントリにあるので、何が変わったかで書き込み方が決まります。

ts
update({ status: 'pending' }, { history: 'push' });
navigation.navigate()が1回

update({ expanded: ['A-102'] });
updateCurrentEntry()で今のエントリを書き換える
  • urlの値が変わるとき:navigation.navigate()を1回呼び、新しいURLとエントリの状態を一緒に渡します。片方だけが変わった画面が見えることはありません。
  • entryの値だけが変わるとき:navigation.updateCurrentEntry()で今のエントリを書き換えます。遷移を伴わないので、どのルーターの下でも動きます。{ history: 'push' }を付けても、新しいエントリは作られません。

URLを変える書き込みは、今のentryの値を新しいエントリへ持ち越します。エントリの状態にほかの定義の名前空間があれば、それもそのまま残ります。

戻る、進む、再読み込みで戻るもの

2つの面は、どちらもブラウザの履歴エントリに載っています。ライブラリが別に記録を取っているわけではないので、ブラウザの操作がそのまま両方に効きます。

  • 戻る/進む:移った先のエントリにあったurlとentryが、一緒に戻ります。
  • 再読み込み:同じエントリのまま読み込み直すので、どちらも残ります。
  • URLを新しいタブで開く:渡るのはURLだけです。urlは同じ値になり、entryは既定値から始まります。
  • リンクのクリック:新しいエントリにはentryの値がまだ無いので、既定値から始まります。
  • セッションの復元:古いスキーマが書いた値が戻ってくることもあります。スキーマに合わない値は、そのフィールドだけが既定値に戻ります。
Playground

2つの面を見比べる

このページのURLと履歴エントリを実際に書き換える、本物のdefinePageStateです。絞り込みはurlに、開いている注文はentryに置いています。

絞り込み
  • 発送済み
  • 未発送
  • 未発送
url
クエリなし
entry
{"expanded":[]}

試してみる

  1. 「A-102」を開きます。entryの値は変わりますが、URLのクエリは変わりません。
  2. 絞り込みを「未発送」に切り替えます。クエリが?status=pendingになり、「A-102」は開いたまま残ります。
  3. 「A-103」も開いてから、ブラウザの戻るを押します。絞り込みが「すべて」に戻り、開いている注文も「A-102」だけに戻ります。
  4. ページを再読み込みしても、絞り込みと開いている注文はそのまま残ります。
k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2