skip to content

In Pinia, why does calling $reset() on a setup store fail, and how do you give a preferences store a working reset to defaults?

level: middleimportance: must knowfreq 58%

answer

  1. only option stores have a factory
  2. state() called again
  3. dev throws, production no-op
  4. return your own $reset
  5. 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 s

In 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 lines
ts
import { 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

for a junior

Remember that $reset() exists for option stores and restores the values state() returns; in a setup store you write your own.

for a middle

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.

for a senior

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.

for a principal

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.