Summary
defineStore(..., { persist }) writes the whole picked state as one JSON blob under one key in localStorage or sessionStorage. It accepts exactly three options — key, storage, pick — and there is no way to map individual fields to their own keys, no way to supply a serializer, and no way to persist to a cookie. Any app that needs a specific storage layout, or needs the server to read one of the values, has to hand-roll persistence outside the store.
Impact (real app)
analyticshq's dashboard and account pages render owner-scoped content on the server, which cannot read localStorage — it authenticates from a cookie (resources/views/dashboard.stx:238). The session store therefore needs three bare localStorage keys and one cookie, which persist cannot express. resources/stores/session.ts:99-122:
const token: StxSignal<string> = useLocalStorage(SESSION_TOKEN_KEY, '')
const user: StxSignal<SessionUser | null> = useLocalStorage(SESSION_USER_KEY, null as SessionUser | null)
const refresh: StxSignal<string> = useLocalStorage(SESSION_REFRESH_KEY, '')
const mirror: StxSignal<string> = sessionCookieSignal(SESSION_COOKIE, { maxAge: 2592000, sameSite: 'Lax' })
…
effect(() => { const t = token(); if (mirror() !== t) mirror.set(t) })That is three separate persisted signals plus a hand-written cookie-mirroring effect, all of which a store-level persist should own.
The serializer gap costs another 20 lines. resources/stores/session.ts:66-87 is sessionMigrateRaw, a one-off migration explained at :16-23:
useLocalStorage still JSON.stringifies on write, but the read path now goes through stxParseStored, which on a parse failure WARNS and returns the raw string instead of throwing. So a legacy raw token no longer breaks the page; it simply logs
"token" holds a value that is not JSONon every load until something rewrites it.
And the same quoting behaviour forced defensive comments in two unrelated files — the pre-paint auth guards at resources/views/account.stx:198-202 and resources/views/dashboard.stx:1745-1750:
a signed-out token is the literal two-character string
"", which is truthy. Testing the raw value would let a signed-out visitor straight through.
A serializer option would have removed all three.
Evidence in stx
packages/stx/src/signals.ts:5891-5934 — the complete implementation:
var persistCfg = storeOptions && storeOptions.persist;
if (persistCfg) {
var pOpts = persistCfg === true ? {} : persistCfg;
var storageKey = pOpts.key || ('stx-store-' + id); // :5894
var storageType = pOpts.storage === 'sessionStorage' ? sessionStorage : localStorage; // :5895
var pick = pOpts.pick || null; // :5896
// read
var saved = storageType.getItem(storageKey);
if (saved) { var parsed = JSON.parse(saved); … } // :5901-5914
// write (debounced)
effect(function() {
var snapshot = {};
for (var wk in result) { if (… (!pick || pick.indexOf(wk) !== -1)) snapshot[wk] = result[wk](); }
…
storageType.setItem(storageKey, JSON.stringify(snapshot)); // :5932
});
}One key, one blob, two hard-coded backends. grep -n 'serializer' packages/stx/src/signals.ts → no match. There is no cookie path anywhere in the persist branch, despite useCookie existing as a runtime global (runtime-globals.ts:53).
Prior art
pinia-plugin-persistedstate: an array of persisters per store, each with its ownkey,storage,pathsand customserializer— exactly the multi-target shape needed here.- VueUse
useStorage(key, default, storage, { serializer }). - Nuxt
useCookieas first-class reactive state, readable by the server render.
Ask
- Accept an array for
persist, so one store can write to several targets:persist: [{ storage: 'localStorage', pick: ['token'], key: 'token' }, { storage: 'cookie', pick: ['token'], key: 'analyticshq_token', cookie: { maxAge, sameSite } }]. - Add per-field key mapping (
keys: { token: 'token', user: 'user' }) so a store can produce bare keys rather than one namespaced blob. - Add a
serializer: { read, write }option, so a raw-string value can be persisted without JSON quoting — and so a legacy layout can be migrated without a hand-written pass. - Add
'cookie'as astoragebackend, delegating to the existinguseCookieruntime global. A store value the server render must read is a common case and is currently impossible to express.
Related: #1795, #1797 (useLocalStorage contract), #1701 (useCookie).