Skip to the solution
MediumFrontend9 min read

Why use storeToRefs with Pinia?

1.0k views

The problem

Question presented to candidate: "A teammate wrote const { count, double } = useCounterStore() in a component, and the numbers on screen never change after the first render. Explain why, what storeToRefs does about it, and when you do not need it."

What a strong answer should cover:

  • A Pinia store is a reactive() object; destructuring reads each property once and copies the current value out, so the copies are no longer connected to the store.
  • storeToRefs(store) returns an object of refs for every state property and getter, each linked to the store, so destructuring keeps reactivity; writing a state ref writes the store.
  • Actions are plain functions bound to the store, so const { increment } = store is fine and storeToRefs deliberately leaves them out.
  • Why not toRefs(store): it also turns actions and internal $ properties into refs (11 keys for a 3-member store in the verified run).
  • Alternatives: just use store.count (no destructuring), toRef(store, 'count') for one value, or computed(() => store.count); getters stay read-only.

Clarifying questions expected:

  • "Is this an option store or a setup store?" (the answer is the same, which is worth saying)

Code / implementation expected: Yes β€” the broken destructuring next to the storeToRefs version, and actions destructured directly.

  • pinia
  • storetorefs

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 10 sections

Read the code

VueThe root cause without Pinia: destructuring a reactive() vs toRefs
<script setup>
// storeToRefs exists because of this: destructuring a reactive object copies
// its current values out, so they stop updating. A Pinia store is a
// reactive() object, so the same thing happens with
// const { count } = useCounterStore()
import { reactive, toRefs, toRef } from "vue";

const store = reactive({ count: 0, step: 1 });

const { count: copiedCount } = store;        // a plain number: frozen at 0
const { count: linkedCount } = toRefs(store); // a ref linked to store.count
const stepRef = toRef(store, "step");         // one property as a ref

function increment() {
  store.count += stepRef.value;
}
</script>

<template>
  <button @click="increment">+{{ stepRef }}</button>
  <button @click="stepRef++">step up</button>
  <p>store.count: {{ store.count }}</p>
  <p>destructured: {{ copiedCount }}</p>
  <p>toRefs: {{ linkedCount }}</p>
</template>
VueBroken vs fixed destructuring of a Pinia store
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useCounterStore } from "../q04/stores";

const store = useCounterStore();

// BROKEN: copies the current values out of the reactive store
const { count: staleCount, double: staleDouble } = store;

// FIXED: refs for state and getters, still linked to the store
const { count, double } = storeToRefs(store);

// Actions are plain functions bound to the store: destructure them directly
const { increment } = store;
</script>

<template>
  <p>broken: {{ staleCount }} / {{ staleDouble }}</p>
  <p>storeToRefs: {{ count }} / {{ double }}</p>
  <button @click="increment()">+1</button>
  <button @click="count = 0">reset via ref</button>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 28 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

How do dynamic routes and route params work in Vue Router?

Continue