---
title: "Why storeToRefs drops your actions · vuejs.school"
description: "You destructure a Pinia store through storeToRefs and the action comes back undefined. Here is what storeToRefs keeps, what it skips, and why."
canonical_url: "https://vuejs.school/community/blog/why-storetorefs-drops-your-actions"
meta:
  "og:description": "You destructure a Pinia store through storeToRefs and the action comes back undefined. Here is what storeToRefs keeps, what it skips, and why."
  "og:image": "https://vuejs.school/_og/d/a_~V2h5IHN0b3JlVG9SZWZzIGRyb3BzIHlvdXIgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Why%2BstoreToRefs%2Bdrops%2Byour%2Bactions%2Cdescription_~WW91IGRlc3RydWN0dXJlIGEgUGluaWEgc3RvcmUgdGhyb3VnaCBzdG9yZVRvUmVmcyBhbmQgdGhlIGFjdGlvbiBjb21lcyBiYWNrIHVuZGVmaW5lZC4gSGVyZSBpcyB3aGF0IHN0b3JlVG9SZWZzIGtlZXBzLCB3aGF0IGl0IHNraXBzLCBhbmQgd2h5Lg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aHktc3RvcmV0b3JlZnMtZHJvcHMteW91ci1hY3Rpb25zIg%2Cs_qmFhncF3uMknALua.png"
  "og:image:alt": "Why storeToRefs drops your actions · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Why storeToRefs drops your actions · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~V2h5IHN0b3JlVG9SZWZzIGRyb3BzIHlvdXIgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Why%2BstoreToRefs%2Bdrops%2Byour%2Bactions%2Cdescription_~WW91IGRlc3RydWN0dXJlIGEgUGluaWEgc3RvcmUgdGhyb3VnaCBzdG9yZVRvUmVmcyBhbmQgdGhlIGFjdGlvbiBjb21lcyBiYWNrIHVuZGVmaW5lZC4gSGVyZSBpcyB3aGF0IHN0b3JlVG9SZWZzIGtlZXBzLCB3aGF0IGl0IHNraXBzLCBhbmQgd2h5Lg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aHktc3RvcmV0b3JlZnMtZHJvcHMteW91ci1hY3Rpb25zIg%2Cs_qmFhncF3uMknALua.png"
  "twitter:image:alt": "Why storeToRefs drops your actions · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## The symptom: the action is undefined

Here is a small cart, written as a setup store: refs become state, computed values become getters, and functions become actions.

*src/stores/cart.ts*

```
import { computed, ref } from 'vue'
import { defineStore } from 'pinia'

export interface CartItem {
  name: string
  price: number
}

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))

  function add(item: CartItem) {
    items.value.push(item)
  }

  return { items, total, add }
})
```

And here is the component that breaks:

*src/components/CartSummary.vue**Breaks***

```
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/cart'

const { items, total, add } = storeToRefs(useCartStore())
</script>

<template>
  <p>{{ items.length }} items, {{ total }} in total</p>
  <button type="button" @click="add({ name: 'Sticker', price: 6 })">Add a sticker</button>
</template>
```

The paragraph renders. The button throws a `TypeError` when you click it, because `add` is `undefined`. If you run `vue-tsc`, it catches the problem earlier: `add` does not exist on the type that `storeToRefs()` returns. That type error is the first clue. The function is not missing by accident; it was never part of the result.

## What storeToRefs returns

`storeToRefs()` is short. This is a simplified copy of [the function in Pinia 3.0.4](https://github.com/vuejs/pinia/blob/v3.0.4/packages/pinia/src/storeToRefs.ts), with the TypeScript types removed:

*storeToRefs, simplified*

```
import { computed, isReactive, isRef, toRaw, toRef } from 'vue'

export function storeToRefs(store) {
  const rawStore = toRaw(store)
  const refs = {}
  for (const key in rawStore) {
    const value = rawStore[key]
    if (value.effect) {
      // a getter: a computed that reads and writes through the store
      refs[key] = computed({
        get: () => store[key],
        set: (next) => { store[key] = next },
      })
    } else if (isRef(value) || isReactive(value)) {
      // state: a ref linked to store[key]
      refs[key] = toRef(store, key)
    }
    // anything else is skipped, including every action
  }
  return refs
}
```

The loop walks over the raw store (the plain object under the reactive wrapper) and sorts each property into one of three cases:

- **Getters.** A computed ref carries an `effect` property and a plain ref does not. Vue has no public way to ask "is this a computed?", so Pinia checks for `effect`. Each getter becomes a computed ref that reads and writes through the store.
- **State.** A ref or a reactive object is state, and `toRef()` turns it into a ref that reads and writes `store[key]`. State that a plugin adds is a ref too, so it comes along.
- **Everything else** is skipped.

An action lands in the last case. It is a plain function: it has no `effect`, and it is neither a ref nor reactive, so the loop never assigns it. The comment above the real function says this outright: methods and non reactive properties are completely ignored.

That is a design decision, not an oversight. A function has no value that changes over time, so there is nothing to keep in sync, and wrapping it in a ref would only make you write `add.value()`.

The TypeScript types follow the same split. Pinia's `StoreToRefs` type is built from three parts: a ref for each state property, a ref for each property a plugin adds to the state, and a computed ref for each getter. Actions are not one of the parts, which is why `vue-tsc` flagged `add` before the browser did.

## Why destructuring the store breaks reactivity

Since actions are fine as they are, you might destructure everything straight from the store and skip `storeToRefs()`. The action then works, but the state and getters lose their reactivity. The store is a `reactive()` object, and destructuring a reactive object reads each property once:

```
const cart = useCartStore()
const { total } = cart // reads cart.total once: a number

cart.add({ name: 'Mug', price: 12 })
console.log(total, cart.total) // 0 12
```

Vue tracks reactivity through property access on the proxy. `cart.total` in a template is a property read that Vue records, so the template updates when the total changes. `const { total } = cart` is also a property read, but it happens once, in setup, and copies the current number into a local constant. Nothing links that constant to the store afterwards. [Pinia's own docs](https://pinia.vuejs.org/core-concepts/#Using-the-store) make the same point with a destructured `name` that never changes.

Arrays and objects hide the bug for a while. `const { items } = cart` gives you the store's reactive array, so `push` from the action still shows up. The day the store replaces the array, with `items.value = []` in a "clear cart" action, your local `items` keeps pointing at the old one.

Props can make this confusing. Since Vue 3.5, `const { title } = defineProps<{ title: string }>()` stays reactive, which suggests destructuring should simply work. It works there because the compiler rewrites every use of `title` into `props.title`. That rewrite only happens for `defineProps`. A store is an ordinary object at runtime, and the compiler does nothing special with it.

## The fix: data through storeToRefs, actions from the store

Split the destructuring by what each piece is. Reactive data goes through `storeToRefs()`. Actions come straight off the store, as in [Pinia's docs](https://pinia.vuejs.org/core-concepts/#Destructuring-from-a-Store).

*src/components/CartSummary.vue**Works***

```
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCartStore } from '../stores/cart'

const cart = useCartStore()
const { items, total } = storeToRefs(cart)
const { add } = cart
</script>

<template>
  <p>{{ items.length }} items, {{ total }} in total</p>
  <button type="button" @click="add({ name: 'Sticker', price: 6 })">Add a sticker</button>
</template>
```

`items` and `total` are now refs linked to the store. In the template they unwrap on their own; in the script you write `total.value`. Writing works too: `items.value = []` writes to the store's state, because a ref made by `toRef()` passes the assignment through.

Taking `add` off the store is safe because Pinia binds actions. Every action is wrapped when the store is created, and the wrapper calls your function with the store as `this` whenever it is called on its own. So `add()` behaves exactly like `cart.add()`, including in option stores whose actions use `this.items`. The wrapper is also what lets `$onAction` subscribers see the call, so destructured actions still show up there.

## The simplest option: use the store as it is

Destructuring is optional. The store is already reactive, so you can use it directly and skip both lines:

*src/components/CartSummary.vue*

```
<script setup lang="ts">
import { useCartStore } from '../stores/cart'

const cart = useCartStore()
</script>

<template>
  <p>{{ cart.items.length }} items, {{ cart.total }} in total</p>
  <button type="button" @click="cart.add({ name: 'Sticker', price: 6 })">Add a sticker</button>
</template>
```

Every read goes through the proxy, so every read is tracked. No `.value`, no question about which helper a property needs, and the `cart.` prefix tells anyone reading the template where the data lives. Start here. Reach for `storeToRefs()` when a component uses the same few properties many times and the prefix gets in the way, or when you need to hand a ref to a composable.

## The same rule in your own composables

The pattern is not specific to Pinia. A composable that returns a `reactive()` object has the same destructuring problem, and the convention in Vue is to return an object of refs and plain functions instead:

*src/composables/useCounter.ts*

```
import { reactive, toRefs } from 'vue'

export function useCounter() {
  const state = reactive({ count: 0, step: 1 })

  function increment() {
    state.count += state.step
  }

  return { ...toRefs(state), increment }
}
```

`toRefs()` gives one ref per property of `state`, so `const { count, increment } = useCounter()` keeps `count` live. The function goes in as it is, because, like a Pinia action, it has nothing to keep in sync.

Vue's `toRefs()` on a store would keep state and getters live too, but it makes a ref for every property: actions included, along with Pinia's own properties such as `$patch` and `$id`. `storeToRefs()` exists to skip those.

The rule matters most when store data flows into a composable. A composable that accepts a ref or a getter stays live:

*src/composables/usePrice.ts*

```
import { computed, toValue, type MaybeRefOrGetter } from 'vue'

const formatter = new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' })

export function usePrice(amount: MaybeRefOrGetter<number>) {
  return computed(() => formatter.format(toValue(amount)))
}
```

`usePrice(total)`, with `total` from `storeToRefs()`, updates with the cart. So does `usePrice(() => cart.total)`. `usePrice(cart.total)` passes a number, read once: the same bug as `const { total } = cart`, in a new place. This time `vue-tsc` stays quiet, because a plain number is a valid `MaybeRefOrGetter<number>`.

**Check it stuck**3 questions from the storeToRefs quiz.

[**Practice now**](https://vuejs.school/learn/pinia/store-to-refs?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**Upgrading a Pinia 3 project to Pinia 4, with the Nuxt and testing packages**](https://vuejs.school/community/blog/upgrading-a-pinia-3-project-to-pinia-4) [Release notes · 7 min read**Pinia 4: what changed and what to do**](https://vuejs.school/community/blog/pinia-4-what-changed-and-what-to-do)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-04-09","description":"You destructure a Pinia store through storeToRefs and the action comes back undefined. Here is what storeToRefs keeps, what it skips, and why.","name":"Why storeToRefs drops your actions","url":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions","breadcrumb":{"@id":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/why-storetorefs-drops-your-actions"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-04-09","description":"You destructure a Pinia store through storeToRefs and the action comes back undefined. Here is what storeToRefs keeps, what it skips, and why.","headline":"Why storeToRefs drops your actions","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~V2h5IHN0b3JlVG9SZWZzIGRyb3BzIHlvdXIgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Why%2BstoreToRefs%2Bdrops%2Byour%2Bactions%2Cdescription_~WW91IGRlc3RydWN0dXJlIGEgUGluaWEgc3RvcmUgdGhyb3VnaCBzdG9yZVRvUmVmcyBhbmQgdGhlIGFjdGlvbiBjb21lcyBiYWNrIHVuZGVmaW5lZC4gSGVyZSBpcyB3aGF0IHN0b3JlVG9SZWZzIGtlZXBzLCB3aGF0IGl0IHNraXBzLCBhbmQgd2h5Lg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aHktc3RvcmV0b3JlZnMtZHJvcHMteW91ci1hY3Rpb25zIg%2Cs_qmFhncF3uMknALua.png","@type":["Article","BlogPosting"],"articleSection":["Deep dive"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/why-storetorefs-drops-your-actions#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Why storeToRefs drops your actions","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~V2h5IHN0b3JlVG9SZWZzIGRyb3BzIHlvdXIgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Why%2BstoreToRefs%2Bdrops%2Byour%2Bactions%2Cdescription_~WW91IGRlc3RydWN0dXJlIGEgUGluaWEgc3RvcmUgdGhyb3VnaCBzdG9yZVRvUmVmcyBhbmQgdGhlIGFjdGlvbiBjb21lcyBiYWNrIHVuZGVmaW5lZC4gSGVyZSBpcyB3aGF0IHN0b3JlVG9SZWZzIGtlZXBzLCB3aGF0IGl0IHNraXBzLCBhbmQgd2h5Lg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aHktc3RvcmV0b3JlZnMtZHJvcHMteW91ci1hY3Rpb25zIg%2Cs_qmFhncF3uMknALua.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~V2h5IHN0b3JlVG9SZWZzIGRyb3BzIHlvdXIgYWN0aW9ucyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Why%2BstoreToRefs%2Bdrops%2Byour%2Bactions%2Cdescription_~WW91IGRlc3RydWN0dXJlIGEgUGluaWEgc3RvcmUgdGhyb3VnaCBzdG9yZVRvUmVmcyBhbmQgdGhlIGFjdGlvbiBjb21lcyBiYWNrIHVuZGVmaW5lZC4gSGVyZSBpcyB3aGF0IHN0b3JlVG9SZWZzIGtlZXBzLCB3aGF0IGl0IHNraXBzLCBhbmQgd2h5Lg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_peach%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aHktc3RvcmV0b3JlZnMtZHJvcHMteW91ci1hY3Rpb25zIg%2Cs_qmFhncF3uMknALua.png"}]}
```