skip to content

In Pinia, a header badge destructures `const { items, itemCount, addItem } = useCartStore()`; which of the three break, and how does storeToRefs fix it?

level: middleimportance: must knowfreq 77%

answer

  1. the store is one reactive object
  2. destructuring reads each value once
  3. arrays survive until replaced
  4. refs for state and getters only
  5. actions are already bound

basics

~20 s

Destructuring a Pinia store copies current values: itemCount freezes, and items keeps the old array once the store replaces it. addItem works because actions stay bound to the store. storeToRefs(store) returns linked refs for state and getters, skipping actions.

solid answer

~40 s

A store is one object wrapped in `reactive()`, so destructuring reads each property once. `itemCount`, a getter, becomes a plain number that never changes. `items` becomes the current reactive array: in-place `push` still shows, but when an action assigns a new array, for example on clearing the cart, the badge keeps the old one. `addItem` is fine, because Pinia runs actions against the store even when they are called on their own. The fix is `const { items, itemCount } = storeToRefs(cart)` plus `const { addItem } = cart`. `storeToRefs()` makes a ref for each state property and a computed for each getter, and skips actions; Vue's `toRefs()` would wrap the actions and `$` methods in refs too.

code

ts · 4 lines
ts
const { items, itemCount, addItem } = useCartStore()
// itemCount: a number read once, never updates
// items: the current array; stale after clear() assigns a new one
// addItem: fine, actions run against the store

go deeper

for a junior

Recall that a store cannot be destructured directly, that storeToRefs keeps state and getters linked, and that actions can be taken straight from the store.

for a middle

Walk through each member: getters freeze, state arrays survive until replaced, actions stay bound. Explain what storeToRefs builds and why toRefs is the wrong tool for a store.

for a senior

Describe how the half-working array case slips past manual testing, and set a team rule: use the store object directly or storeToRefs, and route writes through actions.

for a principal

Weigh short local names against traceability: storeToRefs makes state easy to write from anywhere, so decide whether direct writes are allowed or only actions change stores.

## Why a destructured store stops updating `useCartStore()` returns one object wrapped in Vue's `reactive()`. Refs placed on it are **unwrapped**: `cart.itemCount` reads as a number, not as a ref, which is why you never write `.value` on a store. Destructuring reads each property **once** and binds the result to a local variable. For a number or a string, that local is a plain value nothing tracks. The general Vue mechanism belongs to Vue's reactivity; what is specific to Pinia is that a store mixes three kinds of member, state, getters and actions, and each behaves differently when destructured. ## The three members, one by one Take a setup store that returns `items` (a `ref([])`), `itemCount` (a `computed`) and `addItem` (a function), plus a `clear()` action that runs `items.value = []`. | Member | Kind | After `const { … } = useCartStore()` | |---|---|---| | `itemCount` | getter | frozen at the number read at destructuring time | | `items` | state holding an array | the reactive array itself: in-place `push` still shows, but after `clear()` assigns a new array the local keeps the old one | | `addItem` | action | works: actions run against the store even when called on their own | `items` is the dangerous one because it **half works**. The badge updates while items are pushed, passes a quick manual test, and then keeps listing stale items once checkout clears the cart by replacing the array. ## What storeToRefs() returns `storeToRefs(store)` walks the store and builds an object of refs: - each **state** property becomes `toRef(store, key)`, a ref that reads and writes the store property; - each **getter** becomes a `computed` that reads `store[key]`; - **state added by plugins** is included when it is a ref or a reactive object; - **actions** and other non-reactive values, such as a plain constant returned from a setup store, are skipped. So `const { items, itemCount } = storeToRefs(cart)` gives two refs that follow the store, including when `items` is replaced. Templates unwrap them automatically; script code uses `.value`. Writes go through: assigning `items.value = []` changes the store for every component that reads it. The returned container is a plain object, not a reactive one, so destructuring it is exactly what it is for; each ref inside still points back into the one shared store. ## Why not Vue's toRefs() `toRefs()` also accepts a reactive object, and a store is one, but it wraps **every** enumerable key in a ref, actions and built-in methods such as `$patch` included. Calling an action would then need `.value()`. Pinia's source describes `storeToRefs()` as similar to `toRefs()` but designed for stores, so methods and non-reactive properties are ignored. ## Actions destructure directly Pinia wraps every action so that, when it is called without the store as `this`, it still runs against the store. That is why the docs say actions can be destructured straight from the store: ```ts const cart = useCartStore() const { items, itemCount } = storeToRefs(cart) const { addItem, clear } = cart ``` ## Choosing a style 1. **Use the store object directly**: `cart.itemCount` in the template, `cart.addItem()` in handlers. No destructuring, nothing to break, and the docs point out this is often enough. 2. **Use `storeToRefs()`** when a component reads several fields and you want short names; take actions from the store itself. 3. **Use `computed(() => cart.itemCount)`** when you need a single derived value in script code. ## Spotting it in review - a store call on the right of a destructuring pattern, `const { … } = useCartStore()`, where the names include state or getters; - `toRefs(useCartStore())`, which links state but turns every action into a ref; - `watch(itemCount, …)` on a destructured getter value, which Vue flags as an invalid watch source in development because a plain number is not reactive; - a template that shows a destructured array's items but never reflects a cleared cart. Interviewers use this question to see whether a candidate knows *why* a store cannot be destructured, and whether they know the edge case: arrays and objects appear to survive destructuring until the store replaces them. A strong answer names all three members correctly instead of saying that destructuring simply breaks everything.

  • Can you write through a ref returned by storeToRefs, for example items.value = []?
    Yes. For state, `storeToRefs()` uses `toRef(store, key)`, so assigning the ref writes the store property and every other reader sees the change. Getters come back as computed refs whose setter assigns to the store, which only works for a getter that is itself writable. Many teams still route writes through actions so they appear as named actions in devtools and in `$onAction` listeners.
  • What does storeToRefs do with a plain constant returned from a setup store?
    It skips it. `storeToRefs()` keeps only values that are refs, reactive objects or computeds; functions and plain values such as a returned `TAX_RATE = 0.2` are left out. So `const { TAX_RATE } = storeToRefs(cart)` gives `undefined`. Read constants from the store object, or better, import them from a plain module.

Destructuring a store's number is like copying the score off a stadium scoreboard onto a napkin: the napkin keeps the score from that moment. storeToRefs gives you a live feed of each scoreboard slot instead.

saying these in an interview costs you the question

  • Destructuring a store is fine because every store property is a ref.
  • Actions must go through storeToRefs too, or they lose their this.
  • Vue's toRefs(store) does exactly the same as storeToRefs(store).
  • A destructured state array is safe because arrays are reactive.
  • storeToRefs returns copies, so writing to them leaves the store unchanged.