In Pinia, why does calling $reset() on a setup store fail, and how do you give a preferences store a working reset to defaults?
answer
- only option stores have a factory
- state() called again
- dev throws, production no-op
- return your own $reset
- defaults factory, not a constant
basics
~20 s$reset() re-runs an option store's state() and patches the fresh values in. A setup store has no state() to re-run, so its built-in $reset throws in development and does nothing in production; return your own $reset built on a defaults factory.
solid answer
~40 sIn an option store `$reset()` calls `state()` again for a fresh object and applies it through a function `$patch`, so subscribers see one `'patch function'` change and components keep their references. A setup store's state is whatever refs its setup function created when it ran once; Pinia has nothing it can call again. Its built-in `$reset` therefore throws `Store "preferences" is built using the setup syntax and does not implement $reset()` in development and is a silent no-op in production. The fix is to define and return a `$reset` function from the setup function, fed by a `defaultPreferences()` factory that builds a new object on each call. Never seed state from a shared constant: `ref(DEFAULTS)` wraps that very object, so edits rewrite the defaults and the reset restores the edited values.
code
ts · 23 linesimport { defineStore } from 'pinia'
import { reactive } from 'vue'
type Theme = 'light' | 'dark' | 'system'
function defaultPreferences() {
return {
theme: 'system' as Theme,
fontSize: 14,
notifications: { email: true, push: true },
mutedChannels: [] as string[],
}
}
export const usePreferencesStore = defineStore('preferences', () => {
const prefs = reactive(defaultPreferences())
function $reset() {
Object.assign(prefs, defaultPreferences())
}
return { prefs, $reset }
})go deeper
Remember that $reset() exists for option stores and restores the values state() returns; in a setup store you write your own.
Explain that $reset() re-runs state() and applies it through one function $patch, and that a setup store's built-in $reset throws in development and silently does nothing in production.
Write a returned $reset backed by a defaults factory, spot the shared-constant trap where reactive proxies rewrite the defaults, and make sure a test covers the reset path.
Decide between per-store hand-written resets and one plugin-level reset for all setup stores, weighing explicit code against a shared mechanism every store depends on.
## What $reset() does in an option store An **option store** declares its state as a function: `state: () => ({ … })`. Pinia keeps that function, and `$reset()` uses it: 1. It calls `state()` again, which builds a brand-new object with the initial values. 2. It applies that object with `$patch((s) => Object.assign(s, fresh))`, a **function patch**. 3. Because it is one patch, `$subscribe` callbacks run **once**, with `type: 'patch function'`, and devtools shows one entry. The store keeps the same reactive state object; only its top-level properties are reassigned. Components that hold the store keep working without re-subscribing. This is also why `state` must be a function that returns a new object every time. If it returned a module-level constant, the store would wrap and mutate that constant, and `$reset()` would copy the edited values back onto themselves. ## Why a setup store cannot do the same A **setup store** is `defineStore('preferences', () => { … })`. Its setup function runs **once**, when the store is first used, and the `ref()`s and `reactive()`s it returns become the state. Pinia never sees a description of the initial values, only the live refs, so it has nothing to rebuild them from. Pinia still installs a `$reset` on every store, and for setup stores it is a placeholder: - in a **development** build it throws: `Store "preferences" is built using the setup syntax and does not implement $reset().`; - in a **production** build it is a **no-op**: the call silently does nothing. The second point is the dangerous one. A "Reset to defaults" button that throws in development is noticed; the same button doing nothing in production can ship if nobody clicks it before release. ## Writing your own $reset The documented fix is to write the function yourself and **return it** from the setup function under the name `$reset`. Pinia assigns returned properties onto the store after its own, so yours replaces the placeholder. Like any returned function it becomes an **action**, so action hooks see it too. The pattern has two parts: - a **defaults factory**, `defaultPreferences()`, that returns a new object on every call; - a `$reset` that writes the factory's output into the existing state, field by field or with `Object.assign` into one `reactive()` object. Writing into the existing refs matters: replacing a `reactive()` object held in a `const` is impossible, and swapping the object behind a ref would change identity for any code that captured the old nested object. ## The shared-defaults trap The most common hand-written reset bug has nothing to do with Pinia's API: | Seed | What happens on edit | What reset restores | |---|---|---| | `ref(DEFAULTS)` with a module constant | `ref()` wraps `DEFAULTS` itself in a reactive proxy, so edits rewrite the constant | the edited values | | `ref(defaultPreferences())` from a factory | edits touch only the store's own object | the real defaults | | option store `state: () => DEFAULTS` | the store wraps and mutates the constant | `$reset()` copies edited values onto themselves | Reactive proxies in Vue write through to the object they wrap, so any default that is a shared object is only a default until the first edit. A factory, or a deep copy taken at seed time, avoids it. ## Option store versus setup store | | Option store | Setup store | |---|---|---| | Built-in `$reset()` | works: re-runs `state()` | development: throws; production: no-op | | Source of initial values | the `state` function | none kept by Pinia | | What you write | nothing | a returned `$reset` using a defaults factory | | What subscribers see | one `'patch function'` call | whatever writes your `$reset` makes | If your hand-written reset makes several direct writes, subscribers with the default flush still get one `'direct'` call for that tick. Some teams instead add a generic reset through a Pinia plugin that snapshots each setup store's initial state; that is a plugin concern. ## Checklist for a reset-to-defaults button 1. Option store: call `prefs.$reset()`; make sure `state()` builds a fresh object each time. 2. Setup store: return your own `$reset` from the setup function, driven by a defaults factory. 3. Reset writes into the existing state; nothing replaces the reactive objects components already hold. 4. The reset path is exercised in development or a test, because the production placeholder fails silently.
- In a Pinia option store, what does a $subscribe callback receive when $reset() runs?One call with `type: 'patch function'` and no `payload`, because `$reset()` applies the fresh `state()` object through a function `$patch` with `Object.assign`. It is not reported as a distinct reset type, so an audit log that wants to label resets has to learn that from the call site, not from the mutation.
- Why is a setup store's missing $reset more dangerous in production than in development?In development the placeholder throws with a message naming the store, so the bug surfaces the first time the button is clicked. In production the placeholder is an empty function: the button does nothing and nothing is logged, so the defect ships unless a test or a user notices.
saying these in an interview costs you the question
- $reset() works the same way in setup stores and option stores.
- Pinia snapshots a store's initial state and $reset restores that snapshot.
- Seeding state with ref(DEFAULTS) keeps DEFAULTS safe from later edits.
- A setup store's $reset throws in production just as it does in development.
- $reset() swaps in a new state object, so components must re-read the store.