保存した形を変える
localStorageの行やCookieは、書いたコードより長く残ります。スキーマを変えたあとも前のコードが書いた行は残っていて、新しいコードがそれを読みます。このページでは、何もしないときに古い行がどう読まれるかと、versionとmigrateで形の変化を引き継ぐ方法を説明します。
このページの内容
何もしなければ、フィールドごとに拾う
古いスキーマが書いた行も、ほかの入力と同じくフィールドごとに読まれます。今のスキーマが受け付けるフィールドは値を保ち、それ以外は既定値に戻ります。
- フィールドを足した:足したフィールドは既定値から始まり、ほかの値はそのまま残ります。これで正しく動きます。
- 制約を厳しくした:新しい制約に合わない値だけが既定値に戻ります。これも正しい動きです。
- フィールドの名前を変えた:古い名前の値は読まれず、新しいフィールドは何も言わずに既定値になります。
- 値の意味を変えた:古い値が新しい意味で読まれるか、合わなければ黙って既定値に戻ります。
後の2つのように、拾うだけでは引き継げない変化には、versionとmigrateを使います。
versionとmigrateを渡す
defineLocalStateとdefineCookieStateは、3つ目の引数にversionとmigrateを取ります。
prefs.tsexport 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より古い行は、次の順に読まれます。
migrate(old, fromVersion)が、古い値を今の形に読み替えます。fromVersionは、その行を書いたときの版です。- 返した値を、ほかの読み込みと同じく、スキーマでフィールドごとに拾います。
- ブラウザのストアが、結果を今の版で書き戻します。
migrateが返すのはスキーマのキーで、ほかのキーを返すと型エラーです。値は古いものをそのまま渡してかまいません。合わない値は、スキーマが拒んで既定値に戻します。
サーバーのparseCookiesも移行して読みますが、書き戻しません。ページは応答にSet-Cookieを書けないからです。ハイドレーションのあとにブラウザが書き戻し、サーバーとブラウザは同じ値を読むので、表示はちらつきません。
次に形を変えるとき
次に形を変えるときは、版を上げて、migrateの2つ目の引数fromVersionで分けます。
prefs.tsexport 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を渡さなければ、何も変わりません。行は値のオブジェクトそのもので、古い行はフィールドごとに拾われます。