@k8ordo/state

保存した形を変える

localStorageの行やCookieは、書いたコードより長く残ります。スキーマを変えたあとも前のコードが書いた行は残っていて、新しいコードがそれを読みます。このページでは、何もしないときに古い行がどう読まれるかと、versionとmigrateで形の変化を引き継ぐ方法を説明します。

このページの内容

何もしなければ、フィールドごとに拾う

古いスキーマが書いた行も、ほかの入力と同じくフィールドごとに読まれます。今のスキーマが受け付けるフィールドは値を保ち、それ以外は既定値に戻ります。

  • フィールドを足した:足したフィールドは既定値から始まり、ほかの値はそのまま残ります。これで正しく動きます。
  • 制約を厳しくした:新しい制約に合わない値だけが既定値に戻ります。これも正しい動きです。
  • フィールドの名前を変えた:古い名前の値は読まれず、新しいフィールドは何も言わずに既定値になります。
  • 値の意味を変えた:古い値が新しい意味で読まれるか、合わなければ黙って既定値に戻ります。

後の2つのように、拾うだけでは引き継げない変化には、versionとmigrateを使います。

versionとmigrateを渡す

defineLocalStateとdefineCookieStateは、3つ目の引数にversionとmigrateを取ります。

prefs.ts
export const prefs = defineLocalState(
  'prefs',
  z.object({
    view: z.enum(['grid', 'table']).default('grid'),
    pageSize: z.number().default(20),
  }),
  {
    version: 1,
    migrate: (old) => ({
      view: old['layout'] === 'list' ? 'table' : 'grid',
      pageSize: old['pageSize'],
    }),
  },
);

この例では、版を宣言する前の行が{ layout: 'list' | 'cards' }の形を持っていました。migrateは、そのlayoutを今のviewに読み替えています。

版を持つ行は、[version, values]の形で保存されます。版の無い行は、版0として読みます。そのため、形が初めて変わったときにversion: 1を宣言すれば、今ある行は0から移行されます。

localStorage: k8ordo-state:prefs
{"layout":"list","pageSize":50}
版を宣言する前の行
[1,{"view":"table","pageSize":50}]
移行して書き戻した行

versionは1以上の整数です。それ以外を渡すと、"prefs" version must be a positive integer, got 0のように定義が投げます。

古い行が読まれる流れ

versionより古い行は、次の順に読まれます。

  1. migrate(old, fromVersion)が、古い値を今の形に読み替えます。fromVersionは、その行を書いたときの版です。
  2. 返した値を、ほかの読み込みと同じく、スキーマでフィールドごとに拾います。
  3. ブラウザのストアが、結果を今の版で書き戻します。

migrateが返すのはスキーマのキーで、ほかのキーを返すと型エラーです。値は古いものをそのまま渡してかまいません。合わない値は、スキーマが拒んで既定値に戻します。

サーバーのparseCookiesも移行して読みますが、書き戻しません。ページは応答にSet-Cookieを書けないからです。ハイドレーションのあとにブラウザが書き戻し、サーバーとブラウザは同じ値を読むので、表示はちらつきません。

次に形を変えるとき

次に形を変えるときは、版を上げて、migrateの2つ目の引数fromVersionで分けます。

prefs.ts
export const prefs = defineLocalState(
  'prefs',
  z.object({
    view: z.enum(['grid', 'table']).default('grid'),
    perPage: z.number().default(20),
  }),
  {
    version: 2,
    migrate: (old, fromVersion) => {
      if (fromVersion === 0) {
        return {
          view: old['layout'] === 'list' ? 'table' : 'grid',
          perPage: old['pageSize'],
        };
      }
      return { view: old['view'], perPage: old['pageSize'] };
    },
  },
);

この例は、版2でpageSizeをperPageに改めたときのものです。版0の行も版1の行も、1回のmigrateで今の形になります。

気をつけること

版を持つ行には、次のような場合があります。

  • 新しい版の行:次のデプロイを先に読み込んだタブが書いた行です。移行せずにフィールドごとに拾い、書き戻しません。その行は、新しいタブのものだからです。
  • migrateが投げた:何も保存されていないものとして既定値で読み、行には触りません。直したmigrateが、次の読み込みでやり直せます。
  • 版を宣言したばかり:行の形が[version, values]に変わります。宣言する前のコードで動いているタブは、再読み込みするまで、その行を何も保存されていないものとして読みます。
  • inlineRead():今の版の行しか返しません。ハイドレーションの前にはスキーマもmigrateも動かないので、ほかの版の行は、ストアが移行するまでnullになります。
  • defineSessionState:versionを取りません。行はタブと一緒に消えるので、デプロイをまたいで開いていたタブの行も、拾うだけで足ります。

versionを渡さなければ、何も変わりません。行は値のオブジェクトそのもので、古い行はフィールドごとに拾われます。

k8ordo

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

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2