Skip to content

storeToRefs()

Extracts refs from a store, making state and getters reactive while preserving their reactivity when destructured.

Signature

ts
function storeToRefs<T extends StoreGeneric>(
  store: T
): StoreToRefs<T>

Parameters

  • store: The store instance to extract refs from

Returns

An object containing reactive refs for all state properties and getters. Actions are excluded since they don't need to be reactive.

Why storeToRefs?

When you destructure a store directly, you lose reactivity:

js
// ❌ This breaks reactivity
const { count, doubleCount } = store

// ✅ This preserves reactivity
const { count, doubleCount } = storeToRefs(store)

Basic Usage

Composition API

vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()

// Extract reactive refs for state and getters
const { count, doubleCount } = storeToRefs(store)

// Actions can be destructured directly (they don't need reactivity)
const { increment, decrement } = store
</script>

Options API

js
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  setup() {
    const store = useCounterStore()
    const { count, doubleCount } = storeToRefs(store)
    
    return {
      count,
      doubleCount,
      increment: store.increment,
      decrement: store.decrement
    }
  }
}

What Gets Extracted

State Properties

All state properties become reactive refs:

js
const store = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter',
    items: []
  })
})

const { count, name, items } = storeToRefs(useCounterStore())
// count: Ref<number>
// name: Ref<string>
// items: Ref<any[]>

Getters

All getters become computed refs:

js
const store = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2,
    isEven: (state) => state.count % 2 === 0
  }
})

const { doubleCount, isEven } = storeToRefs(useCounterStore())
// doubleCount: ComputedRef<number>
// isEven: ComputedRef<boolean>

Actions Are Excluded

Actions are not included in the returned object since they don't need reactivity:

js
const store = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++
    }
  }
})

const refs = storeToRefs(useCounterStore())
// refs.increment is undefined

// Get actions directly from store
const { increment } = useCounterStore()

Advanced Usage

Selective Extraction

js
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const store = useUserStore()

// Extract only what you need
const { name, email } = storeToRefs(store)
const { updateProfile, logout } = store

Renaming During Extraction

js
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const store = useUserStore()
const storeRefs = storeToRefs(store)

// Rename during destructuring
const {
  name: userName,
  email: userEmail
} = storeRefs

Multiple Stores

js
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const userStore = useUserStore()
const cartStore = useCartStore()

// Extract from multiple stores
const { name, email } = storeToRefs(userStore)
const { items, total } = storeToRefs(cartStore)

// Get actions
const { login, logout } = userStore
const { addItem, removeItem } = cartStore

Computed Properties

js
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const store = useUserStore()
const { name, email } = storeToRefs(store)

// Create computed based on store refs
const displayName = computed(() => {
  return name.value || email.value || 'Anonymous'
})

const isLoggedIn = computed(() => {
  return !!name.value && !!email.value
})

Watchers

js
import { watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const store = useUserStore()
const { name, email } = storeToRefs(store)

// Watch individual refs
watch(name, (newName, oldName) => {
  console.log(`Name changed from ${oldName} to ${newName}`)
})

// Watch multiple refs
watch([name, email], ([newName, newEmail], [oldName, oldEmail]) => {
  console.log('User info changed:', { newName, newEmail })
})

TypeScript

Type Inference

ts
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)

// TypeScript automatically infers:
// count: Ref<number>
// doubleCount: ComputedRef<number>

Explicit Typing

ts
import type { Ref, ComputedRef } from 'vue'
import type { StoreToRefs } from 'pinia'
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

type CounterStore = ReturnType<typeof useCounterStore>
type CounterRefs = StoreToRefs<CounterStore>

const store = useCounterStore()
const refs: CounterRefs = storeToRefs(store)

// Or extract specific types
const count: Ref<number> = refs.count
const doubleCount: ComputedRef<number> = refs.doubleCount

Generic Helper

ts
function useStoreRefs<T extends StoreGeneric>(useStore: () => T) {
  const store = useStore()
  const refs = storeToRefs(store)
  
  return {
    store,
    refs,
    ...refs
  }
}

// Usage
const { store, refs, count, doubleCount } = useStoreRefs(useCounterStore)

Common Patterns

Form Binding

vue
<template>
  <form @submit.prevent="saveUser">
    <input v-model="name" placeholder="Name" />
    <input v-model="email" placeholder="Email" />
    <button type="submit">Save</button>
  </form>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const store = useUserStore()
const { name, email } = storeToRefs(store)
const { saveUser } = store
</script>

Conditional Rendering

vue
<template>
  <div>
    <div v-if="isLoading">Loading...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <h1>{{ title }}</h1>
      <p>{{ content }}</p>
    </div>
  </div>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useContentStore } from '@/stores/content'

const store = useContentStore()
const { isLoading, error, title, content } = storeToRefs(store)
</script>

List Rendering

vue
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }} - {{ item.price }}
      <button @click="removeItem(item.id)">Remove</button>
    </li>
  </ul>
  <p>Total: {{ total }}</p>
</template>

<script setup>
import { storeToRefs } from 'pinia'
import { useCartStore } from '@/stores/cart'

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

Performance Considerations

Lazy Extraction

js
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useExpensiveStore } from '@/stores/expensive'

// Only extract when needed
const expensiveRefs = computed(() => {
  const store = useExpensiveStore()
  return storeToRefs(store)
})

// Access when needed
const expensiveData = computed(() => expensiveRefs.value.data)

Selective Extraction

js
import { storeToRefs } from 'pinia'
import { useLargeStore } from '@/stores/large'

// Don't extract everything if you only need a few properties
const store = useLargeStore()
const allRefs = storeToRefs(store) // ❌ Extracts everything

// Better: extract only what you need
const { specificProp } = storeToRefs(store) // ✅ More efficient

Best Practices

1. Extract State and Getters, Not Actions

js
// ✅ Good
const { count, doubleCount } = storeToRefs(store)
const { increment, decrement } = store

// ❌ Avoid - actions don't need reactivity
const { count, doubleCount, increment } = storeToRefs(store) // increment is undefined

2. Use Descriptive Names

js
// ✅ Good - clear what store the data comes from
const { name: userName, email: userEmail } = storeToRefs(userStore)
const { items: cartItems, total: cartTotal } = storeToRefs(cartStore)

// ❌ Confusing - unclear source
const { name, email } = storeToRefs(userStore)
const { name: itemName } = storeToRefs(productStore) // name conflict

3. Extract Early in Setup

js
// ✅ Good - extract at the top of setup
const store = useMyStore()
const { data, isLoading } = storeToRefs(store)
const { fetchData } = store

// Then use in computed, watchers, etc.
const processedData = computed(() => {
  return data.value?.map(item => ({ ...item, processed: true }))
})

4. Combine with Other Composables

js
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'

function useUserProfile() {
  const router = useRouter()
  const store = useUserStore()
  const { user, isLoading } = storeToRefs(store)
  const { fetchUser, updateUser } = store
  
  const navigateToProfile = () => {
    router.push('/profile')
  }
  
  return {
    user,
    isLoading,
    fetchUser,
    updateUser,
    navigateToProfile
  }
}

Troubleshooting

Lost Reactivity

js
// ❌ Problem: Direct destructuring loses reactivity
const { count } = useCounterStore()

// ✅ Solution: Use storeToRefs
const { count } = storeToRefs(useCounterStore())

Undefined Actions

js
// ❌ Problem: Actions are not in storeToRefs result
const { increment } = storeToRefs(store) // increment is undefined

// ✅ Solution: Get actions from store directly
const { increment } = store

TypeScript Errors

ts
// ❌ Problem: Type errors with wrong extraction
const store = useMyStore()
const { nonExistentProp } = storeToRefs(store) // TypeScript error

// ✅ Solution: Only extract existing properties
const { existingProp } = storeToRefs(store)

See Also

Released under the MIT License.