skip to content

In a Pinia option store, what does store.$state = savedPrefs actually do to the existing state, and how does it differ from $patch(savedPrefs)?

level: seniorimportance: nice to knowfreq 20%

answer

  1. you cannot swap the object
  2. setter wraps a function patch
  3. Object.assign, top level only
  4. missing keys keep old values
  5. docs and source disagree

basics

~20 s

Assigning $state keeps the same state object and runs a function $patch that Object.assigns each top-level key: nested objects are replaced whole, absent keys keep their values. $patch(savedPrefs) deep-merges plain objects and reports a payload.

solid answer

~40 s

You cannot swap a store's state object without breaking reactivity, so the `$state` setter does not try. It calls `$patch(($state) => Object.assign($state, savedPrefs))`: the state keeps its identity, each top-level key in `savedPrefs` is overwritten whole, and keys `savedPrefs` lacks keep their current values. Subscribers get one `'patch function'` call without a payload. `$patch(savedPrefs)` is the object form: it merges recursively into plain nested objects, so a saved `notifications: { push: false }` keeps `email`, where the `$state` assignment would drop it, and subscribers get `'patch object'` with the payload. Pinia's docs describe the setter as calling `$patch` with the object; the 4.0.3 source uses the function form, so trust the source. To make the store equal the saved copy with defaults filling the gaps, build that complete object first, then apply it.

code

ts · 13 lines
ts
import { usePreferencesStore } from './preferences'

const prefs = usePreferencesStore()
// before: { theme: 'dark', fontSize: 18, notifications: { email: true, push: true } }

const saved = { theme: 'light' as const, notifications: { push: false } }

prefs.$patch(saved)
// { theme: 'light', fontSize: 18, notifications: { email: true, push: false } }

// @ts-expect-error: $state is typed as the full state
prefs.$state = saved
// { theme: 'light', fontSize: 18, notifications: { push: false } }  (email gone)

go deeper

for a junior

Know that assigning $state does not replace the state object; Pinia patches the existing one instead.

for a middle

Explain that the setter runs a function $patch with Object.assign, so top-level keys are overwritten and absent keys stay.

for a senior

Contrast it with $patch's recursive merge, catch lost nested keys and aliasing when loading saved data, and trust the source over the docs on the mutation type.

for a principal

Define how saved or server data is applied to stores across the app: overlay, full replacement with defaults, or reset then patch, and make it one helper.

## Why the state object cannot really be replaced A store's state lives in the root state, under the store's id, as one reactive object. The store's own properties are linked to that object's properties, and components, getters and watchers hold references into it. Swapping in a different object would leave all of them pointing at the old one. So Pinia's docs say you **cannot exactly replace** a store's state, and the `$state` setter does something else. ## What the setter does (Pinia 4.0.3 source) Assigning `store.$state = savedPrefs` runs: 1. `$patch(($state) => Object.assign($state, savedPrefs))`, a **function patch**; 2. `Object.assign` copies each **own top-level key** of `savedPrefs` onto the existing state object; 3. subscribers get **one** call with `type: 'patch function'` and **no payload**. The consequences: - **identity is kept**: `store.$state` is the same object before and after, so nothing downstream breaks; - **top-level keys are overwritten whole**: a saved `notifications` object replaces the current one, so nested keys missing from the saved copy disappear; - **absent keys are left alone**: if `savedPrefs` has no `fontSize`, the current (possibly edited) value stays; nothing is reset to defaults; - **values are shared by reference**: nested objects and arrays from `savedPrefs` become the store's, so mutating the store later also mutates `savedPrefs` unless it was a fresh copy. ## Docs versus source Pinia's state guide shows `store.$state = { count: 24 }` and says it "internally calls `$patch()`" with the object form, `store.$patch({ count: 24 })`. For a flat object the result is the same. For nested objects and for subscribers it is not: the pinned source uses the **function** form with `Object.assign`. When the docs and the code disagree, the code is what runs. ## `$state =` versus `$patch(object)` | | `store.$state = saved` | `store.$patch(saved)` | |---|---|---| | State object identity | kept | kept | | Nested plain objects | replaced whole | merged key by key | | Arrays | replaced | replaced | | Keys absent from `saved` | kept | kept | | `mutation.type` | `'patch function'` | `'patch object'` | | `mutation.payload` | none | `saved` | | TypeScript | `$state` is typed as the full state, so a partial object does not compile | accepts a deep partial | Neither form deletes keys, and neither resets anything. ## Loading saved preferences correctly The preferences panel loads a saved copy from the server. Which call is right depends on what "apply" means: - **Overlay the saved values, keep everything else**: `$patch(saved)`. Nested settings merge, and the audit log gets a payload naming what was applied. - **Make the store equal to the saved copy, with defaults for anything missing**: build the full object first, then assign it to `$state`. A spread such as `{ ...defaultPreferences(), ...saved }` fills only top-level gaps; a partial nested group in `saved` still needs its own merge with the defaults. Alternatively `$reset()` an option store, then `$patch(saved)`, which merges nested groups onto the defaults at the cost of two subscriber calls. - **Avoid aliasing**: clone the server response before applying it if other code keeps using it. ## Where the root state fits Assigning `pinia.state.value` sets the initial state of **every** store at once; it is the mechanism used when the server's state is handed to the client. That is a different, application-wide operation; `store.$state` touches a single store and always goes through `$patch`. ## Mistakes to avoid - **Calling it a replace.** Assigning `$state` never removes keys and never resets absent ones; it overwrites what it is given. - **Assuming it merges like `$patch`.** A partial nested group in the assigned object wipes the sibling keys in that group. - **Silencing the type error.** When TypeScript rejects a partial object for `$state`, the fix is usually `$patch`, not a cast. - **Relying on the mutation type from the docs.** An audit log that expects `'patch object'` with a payload for `$state` assignments gets `'patch function'` and nothing else. In short: use `$patch(object)` to overlay values, assign `$state` only with a complete object, and remember that both keep the same reactive state underneath.

  • What does an audit log built on Pinia's $subscribe see when code assigns store.$state?
    One synchronous call with `type: 'patch function'` and no `payload`, because the setter runs a function `$patch` around `Object.assign`. The log cannot tell it apart from any other function patch or an option store's `$reset()`; only a diff against its own snapshot shows what changed.
  • Why does a Pinia store's state share objects with the saved data after $state = saved?
    `Object.assign` copies references, not values: `saved.notifications` itself becomes the store's `notifications`, wrapped by the store's reactive proxy. Edits through the store then write into `saved.notifications`. Clone the saved data first, for example with a JSON round trip, if anything else still reads it.

Assigning $state is like pouring a saved form into the open one field by field: each field the saved copy names is overwritten whole, fields it lacks keep what is typed, and it is still the same form on screen.

saying these in an interview costs you the question

  • Assigning $state swaps in a new state object, breaking existing references.
  • Assigning $state deep-merges nested objects exactly like $patch with an object.
  • Keys missing from the assigned object are reset to their defaults.
  • Subscribers see a $state assignment as 'patch object' with the payload.
  • Assigning $state removes keys that the new object does not contain.