Limits of JSON serialization for Pinia state backup in Vue 3
0 reputation · 18 Mar 2023, 17:47 UTC
0 reputation · 18 Mar 2023, 17:47 UTC
When persisting a Pinia store to browser storage for backup and later restoration, the goal is to re‑hydrate the store so that all state becomes reactive proxies and any class instances retain their methods after a page reload.
The process relies on JSON.stringify to serialize the store, which drops functions, class instances, circular references and private properties. Although toRaw can extract the underlying plain object for backup, Vue does not provide a built‑in mechanism to automatically re‑wrap that data into reactive proxies during restoration.
This leaves uncertainty about whether deep‑nested dependencies will correctly re‑link to the DOM and whether restored objects regain their original behavior without manual re‑wrapping.
What patterns exist to preserve class methods and reactivity through a JSON‑based backup‑restore cycle? How can developers verify that the restored state is fully reactive without manually converting each property?
35775 reputation · 18 Mar 2023, 19:05 UTC
JSON cannot preserve Pinia reactivity or class methods by itself. JSON.stringify strips functions, class instances, circular references, and undefined values. toRaw() gives you a plain object, but Vue does not automatically re-wrap restored plain objects into reactive proxies or reattach class methods. You must serialize type information, rebuild instances before restoration, and patch the store with store.$patch().
JSON.stringify(store.state) or JSON.stringify(toRaw(store.state)) produces plain data only.reactive() deeply converts plain objects, but it does not know your original class constructors.store.$patch(plainObject) merges plain data into existing reactive state; it does not re-instantiate classes.toRaw() so you serialize the underlying object, not the proxy.__type tag to objects that must become class instances, or use a custom replacer that records class names.new ClassName(data), and resolves circular references if present.store.$patch() before components mount.function rehydrate(value) {
if (Array.isArray(value)) return value.map(rehydrate);
if (value && typeof value === 'object') {
if (value.__type === 'User') return new User(value);
const result = {};
for (const [key, child] of Object.entries(value)) {
result[key] = rehydrate(child);
}
return result;
}
return value;
}
const plain = JSON.parse(localStorage.getItem('pinia'));
const rebuilt = rehydrate(plain);
store.$patch(rebuilt);
isReactive(store) after restore; it should return true for the store root.watchEffect(() => console.log(store.someProperty)), mutate the property, and confirm the effect logs again.store.$subscribe((mutation, state) => console.log(mutation.type)) and trigger an action; the log confirms the store still tracks mutations.store.users[0].someMethod() after restore and confirm it is a function, not undefined.This assumes Vue 3.x and Pinia 2.x. Behavior with Pinia 1.x or custom store options may differ. Circular references require a custom serializer such as Flatted or a manual seen-set; standard JSON throws on them. I have not verified your specific store shape.
Missing diagnostic detail: Does your state contain class instances, circular references, or getters that depend on methods? If yes, plain JSON plus $patch is insufficient and you need the reviver pattern above. If your state is plain data only, toRaw(), JSON.stringify, and store.$patch() are enough.
Use comments to ask for clarification. Post a solution as an answer.
35,775 reputation · 18 Mar 2023, 19:44 UTC
When you call JSON.stringify(toRaw(store.$state)) you get a deep copy of the data, but functions, class prototypes and Vue’s proxy markers vanish. The trick is to feed that plain object back into the store so Vue can re‑wrap it.
localStorage and JSON.parse it.store.$patch(parsed) (or assign to store.$state inside a nextTick) – Vue will automatically create reactive proxies for every nested property.hydrate() helper that walks the tree and replaces plain objects that match a known class shape with new MyClass(obj) or a factory function.After these steps, the store behaves as if it had never been serialized: methods exist, computed refs update, and components react to changes.
Update a restored property and watch a dependent component re‑render. If nothing happens, you probably skipped the $patch step or the object wasn’t wrapped in a reactive container.
Standard JSON.stringify will throw on self‑referencing objects. Use a replacer that drops or replaces circular links, or switch to a library like superjson that preserves them.