storeToRefs
Destructure state and getters from a store without losing reactivity, and take actions straight from the store.
What you'll learn
When you finish, you can do these.- 1Explain why a value pulled out of a store stops updating
- 2Keep short names linked to a store with storeToRefs()
- 3Take actions such as add() straight from the store
- 4Spot and fix a frozen total and a missing action
Picture a shop page with a cart, built with Pinia, Vue's official store library. The cart lives in a store, one place every component can read and change, and the page keeps that store in a variable, cart. Its template writes cart.items, the things in the cart, and cart.total, their prices added up, again and again. Pulling both out into short names with destructuring, const { items, total } = cart, looks tidy. The total is right at first, then silently stops following the store.
storeToRefs(), a function from pinia, gives you the short names and keeps them linked. You need one idea from the Setup stores lesson: a store can be one function, in which refs (boxes that hold a value and tell Vue when it changes) are the state, computed values such as the total are the getters, and plain functions are the actions. The Code tab holds the cart's store, written that way in stores/cart.ts, and the page, App.vue, with an Add sticker button. One line on the page follows the store and one stays stuck: by the end you can say why.
Destructuring copies the value
A number pulled out of a store into a short name is a copy, and a copy never updates. A store is a reactive object: Vue notices each read that goes through it. {{ cart.total }} reads the store at every render, so the page updates when the total changes. Destructuring reads the property once and puts its value into a new variable that Vue knows nothing about.
const { total } = cartCopies the number the total holds at that moment. The variable is a plain number and never changes again.
storeToRefs(cart)Returns a ref for each piece of state and each getter. The ref reads the store every time, so total.value follows every change.
App.vue in the Code tab makes such a copy on purpose. This line takes total out of the store and names the copy frozen:
const { total: frozen } = cart // a plain number, copied once
The copy is made once, when App's <script setup> runs and the cart is still empty, so it holds 0. Click Add sticker in the preview a few times: the total on the first line counts up, and the line Copied at the start stays at $0.
storeToRefs() keeps the link
storeToRefs() gives you short names without the copy. It takes a store and returns an object with one ref for each piece of state and each getter, and you destructure that object instead of the store. In App.vue, that happens right under const cart = useCartStore(), the line that gets the store:
const { items, total } = storeToRefs(cart) // refs that stay linked to the store
Each ref reads from the store, and a ref for state also writes into it, so it behaves like any other ref: total.value in script, {{ total }} in the template. The first line of the page, the count and the total, follows every click on Add sticker.
You do not always need it. Reading through the store, {{ cart.total }} in a template or computed(() => cart.total) in script, stays reactive with no helper. The Summary line at the bottom of the preview works this way: it comes from a second component, CartSummary.vue, which reads cart.items, cart.total and cart.note. note is the store's other piece of state, a text note that starts empty.
Reach for storeToRefs() when you want short names, or when something needs a real ref, such as v-model, which ties an input to a ref so that typing writes into it.
Vue's own toRefs() also turns each property of a reactive object into a ref, but on a store it wraps everything: each action, and Pinia's own properties such as $id, $patch and $subscribe. A destructured action then arrives as a ref, not a function. storeToRefs() keeps only state and getters.
Actions come straight from the store
Actions, the store's functions such as add(), need no helper. They are functions, not reactive data, so storeToRefs() leaves them out on purpose. Take them from the store itself, as App.vue does:
const { add } = cart // actions come straight from the store
The Add sticker button calls add('Sticker', 6), and a $6 sticker lands in the cart. Pinia ties every action to its own store, so add still changes the right cart after you take it out. Calling cart.add('Sticker', 6) works as well.
What goes wrong
Two mistakes catch most people. The first is taking total straight out of the store. Nothing warns, the first render looks right, and the number never moves: click Add sticker and App's total stays at $0 while the summary under it counts up.
const { total } = cart
const { total } = storeToRefs(cart)
Taken from storeToRefs(cart), App's total counts up with the summary.
The second mistake goes the other way: taking everything through storeToRefs(), actions included.
const { total, add } = storeToRefs(cart)
storeToRefs() skips actions, so add is undefined. The page renders fine, and the first click on Add sticker throws _unref(...) is not a function in the preview. The odd name comes from the JavaScript Vue makes from your template; called from your own script, the same mistake reads add is not a function. Split the line in two: refs from storeToRefs(cart), actions from cart.
const { total } = storeToRefs(cart)
const { add } = cart
Add sticker works again, and the total follows each click.
What to remember
- Destructuring a store copies values. A number or a string copied out of a store never changes again.
storeToRefs(store)returns refs for state and getters that stay linked to the store. Destructure that instead.- Reading through the store, as
cart.totalorcomputed(() => cart.total), stays reactive and needs no helper. - Actions come straight from the store,
const { add } = cart, becausestoreToRefs()leaves them out. - An array pulled out of a store seems to stay linked until an action puts a new array in its place.
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Destructure without the helper. Change
storeToRefs(cart)tocarton the line that takesitemsandtotal. Click Add sticker three times: App's total stays at $0 while the summary says $18. The count in App still goes up, becauseitemsis the same array the store pushes into. PutstoreToRefsback. - Add an action that replaces the array. In the store, add
function removeLast() { items.value = items.value.slice(0, -1) }and return it. In App, take it from the store next toaddand add<button @click="removeLast()">Remove last</button>. Each click lowers the count and the total in App and in the summary. - Bind a store ref with
v-model. In App, change thestoreToRefs(cart)line toconst { items, total, note } = storeToRefs(cart)and add<input v-model="note" placeholder="Gift note">. Type for Sam: the summary underneath shows note: for Sam, because the ref writes into the store that CartSummary reads.