Fix: Pinia State Not Reactive — Store Changes Not Updating the Component
Quick Answer
How to fix Pinia store state not updating components — storeToRefs for destructuring, $patch for partial updates, avoiding reactive() wrapping, getters vs computed, and SSR hydration.
The Problem
A Pinia store state change doesn’t re-render the component:
<script setup>
import { useUserStore } from '@/stores/user';
const store = useUserStore();
const { name } = store; // Destructured — loses reactivity
</script>
<template>
<p>{{ name }}</p> <!-- Shows initial value, never updates -->
</template>Or a store action updates state but the template doesn’t reflect it:
// store
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
actions: {
addItem(item) {
this.items.push(item); // Pushes to array — is this reactive?
}
}
});Or a getter returns stale data after state changes:
getters: {
totalPrice: (state) => {
return state.items.reduce((sum, item) => sum + item.price, 0);
// Returns 0 even though items have been added
}
}Why This Happens
Pinia state is reactive under the hood (using Vue’s reactive()), but reactivity is lost when values are extracted incorrectly:
- Plain destructuring breaks reactivity —
const { name } = storeextracts the current string value, not a reactive reference. The variablenamewon’t update when the store changes. - Wrapping the store in
reactive()— Pinia stores are already reactive. Wrapping withreactive()or nesting inside anotherreactive()can break Pinia’s internal reactivity tracking. - Using
computed()incorrectly — accessing store state inside acomputed()works correctly, but setting it without an action bypasses the store’s reactivity system. - SSR mismatches — server-rendered state may not hydrate correctly on the client, causing the store to have stale or undefined values.
- Composition API store with
refvs plain values — in the Setup store syntax, you must returnref()s andcomputed()s to keep them reactive; plain values are not reactive.
Fix 1: Use storeToRefs to Destructure Reactively
The most common fix — use storeToRefs() to extract reactive refs from the store:
<script setup>
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const store = useUserStore();
// WRONG — plain destructuring loses reactivity
const { name, email } = store;
// CORRECT — storeToRefs preserves reactivity
const { name, email, isLoggedIn } = storeToRefs(store);
// name, email, isLoggedIn are Refs — they update when the store changes
// Actions don't need storeToRefs — they're functions, not reactive
const { login, logout, updateProfile } = store;
</script>
<template>
<!-- These work correctly — name and email are reactive refs -->
<p>{{ name }}</p>
<p>{{ email }}</p>
<button @click="logout">Logout</button>
</template>Alternative — access state directly through the store (always reactive):
<script setup>
import { useUserStore } from '@/stores/user';
const store = useUserStore();
// Don't destructure — access through store
</script>
<template>
<!-- Always reactive — reads from the store on each render -->
<p>{{ store.name }}</p>
<p>{{ store.email }}</p>
</template>computed() for derived state:
<script setup>
import { computed } from 'vue';
import { useUserStore } from '@/stores/user';
const store = useUserStore();
// Derived state from store — correctly reactive
const displayName = computed(() =>
store.name || store.email || 'Anonymous'
);
</script>Fix 2: Update State Correctly in Actions
Pinia state mutations inside actions work with both direct assignment and $patch:
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
discount: 0,
loading: false,
}),
actions: {
// CORRECT — direct mutation inside an action
addItem(item: CartItem) {
this.items.push(item); // Array mutation IS reactive within actions
// This works because Pinia wraps state in reactive()
},
removeItem(id: string) {
this.items = this.items.filter(item => item.id !== id);
// Or: mutate in place
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) this.items.splice(index, 1);
},
// Async actions
async fetchCart() {
this.loading = true;
try {
const data = await api.getCart();
this.items = data.items; // Replace the array — reactive
this.discount = data.discount;
} finally {
this.loading = false;
}
},
// $patch for multiple state changes atomically
applyPromoCode(code: string) {
this.$patch({
discount: 0.15,
promoCode: code,
promoApplied: true,
});
// One reactive update instead of three
},
// $patch with function (useful for complex updates)
addMultipleItems(newItems: CartItem[]) {
this.$patch((state) => {
state.items.push(...newItems);
state.lastUpdated = new Date();
});
},
},
});Mutating state directly outside actions (only in non-strict mode):
const store = useCartStore();
// Works in non-strict mode (default), but prefer using actions
store.discount = 0.1;
// $patch is always allowed (no strict mode restriction)
store.$patch({ discount: 0.1 });
// Enable strict mode to prevent direct state mutations:
// In Pinia setup: createPinia() or in nuxt config
// pinia.use(({ store }) => { store.$state = readonly(store.$state) })Fix 3: Fix Setup Store (Composition API) Reactivity
The Setup store syntax uses Vue’s Composition API directly. You must return ref()s and computed()s — plain values won’t be reactive:
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// State — must use ref() or reactive()
const count = ref(0);
const history = ref<number[]>([]);
// WRONG — plain variable is not reactive
// let label = 'Counter'; // Not reactive — changes don't update components
// CORRECT — ref for primitives
const label = ref('Counter');
// Getters — use computed()
const doubled = computed(() => count.value * 2);
const historyLength = computed(() => history.value.length);
// Actions — plain functions
function increment() {
count.value++;
history.value.push(count.value);
}
function reset() {
count.value = 0;
history.value = [];
}
// MUST return all state, getters, and actions
return {
count,
history,
label,
doubled,
historyLength,
increment,
reset,
};
// Anything NOT returned is private to the store
});storeToRefs works the same with Setup stores:
<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const store = useCounterStore();
const { count, doubled, label } = storeToRefs(store);
const { increment, reset } = store;
</script>Fix 4: Fix Getters Not Updating
Getters (Options API) and computed() (Setup API) are cached and re-evaluate when their reactive dependencies change:
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
}),
getters: {
// CORRECT — reads this.items, which is reactive
totalPrice(): number {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
// CORRECT — getter using another getter
formattedTotal(): string {
return `$${this.totalPrice.toFixed(2)}`;
},
// CORRECT — getter with argument (returns a function)
itemById: (state) => (id: string) => {
return state.items.find(item => item.id === id);
},
},
});
// Usage — call with argument
const item = cartStore.itemById('product-42');Getter not updating — common mistake:
getters: {
// WRONG — caches the result, but the cached result is an array reference
// Mutations to the array items (not the array itself) may seem to not update
sortedItems: (state) => state.items.sort((a, b) => a.name.localeCompare(b.name)),
// .sort() mutates state.items in-place AND returns the same array reference
// Vue may not detect this as a change
// CORRECT — create a new sorted array
sortedItems: (state) => [...state.items].sort((a, b) => a.name.localeCompare(b.name)),
},Fix 5: Reset Store State
Sometimes stale state is the issue — use $reset() to restore the initial state:
const store = useCartStore();
// Reset to initial state (Options API stores only)
store.$reset();
// Setup stores don't have $reset() by default — implement it manually
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const initialState = { count: 0 };
function $reset() {
count.value = initialState.count;
}
return { count, $reset };
});
// Or use a plugin to add $reset to all Setup stores:
// https://pinia.vuejs.org/cookbook/composing-stores.htmlSubscribe to store changes for debugging:
const store = useCartStore();
// Watch all state changes
store.$subscribe((mutation, state) => {
console.log('Pinia mutation:', mutation.type);
console.log('New state:', JSON.parse(JSON.stringify(state)));
// mutation.type: 'direct' | 'patch object' | 'patch function'
});
// Watch specific state with Vue's watch
import { watch } from 'vue';
watch(
() => store.items.length,
(newLen, oldLen) => {
console.log(`Cart items: ${oldLen} → ${newLen}`);
}
);Fix 6: Fix Pinia with Vue Router and SSR
In Nuxt or SSR setups, Pinia stores must be initialized after the Vue app is created:
// plugins/pinia-hydration.ts (Nuxt)
export default defineNuxtPlugin((nuxtApp) => {
// Access the Pinia instance
const pinia = nuxtApp.$pinia;
// Hydrate from server state
if (process.client && nuxtApp.payload.pinia) {
pinia.state.value = nuxtApp.payload.pinia;
}
});Store not available during SSR — use useNuxtApp() or useStore() inside setup:
<!-- WRONG — store used at module level (before app is initialized) -->
<script>
const store = useUserStore(); // Error during SSR
</script>
<!-- CORRECT — store used inside setup() or <script setup> -->
<script setup>
const store = useUserStore(); // Called during component setup — safe
</script>Pinia with Vue Router navigation guards:
// router/index.ts
import { createRouter } from 'vue-router';
const router = createRouter({ ... });
router.beforeEach(async (to) => {
// Get the store INSIDE the guard, not outside
const authStore = useAuthStore(); // Correct — accessed after app is set up
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return '/login';
}
});Fix 7: Debug Pinia Reactivity
Use Vue DevTools’ Pinia panel to inspect store state and track mutations:
// Add debugging to a store
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as CartItem[],
}),
actions: {
addItem(item: CartItem) {
console.group('addItem action');
console.log('Before:', JSON.parse(JSON.stringify(this.$state)));
this.items.push(item);
console.log('After:', JSON.parse(JSON.stringify(this.$state)));
console.groupEnd();
},
},
});
// Subscribe to all actions globally (for logging)
const pinia = createPinia();
pinia.use(({ store }) => {
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Pinia] Action: ${store.$id}.${name}`, args);
after((result) => {
console.log(`[Pinia] Action ${name} completed`, result);
});
onError((error) => {
console.error(`[Pinia] Action ${name} failed`, error);
});
});
});Still Not Working?
Pinia not installed as a plugin — app.use(pinia) must be called before any store is accessed. In Nuxt, this is handled automatically. In plain Vue 3, ensure the setup order:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia); // Must come before any store usage
app.use(router);
app.mount('#app');Multiple Pinia instances — if you accidentally create multiple Pinia instances (e.g., one in the app, one in tests), stores from one instance aren’t reactive in the other. Ensure a single Pinia instance is shared across the app.
reactive() wrapping a store — don’t wrap a Pinia store in reactive(). Pinia stores are already reactive; double-wrapping can break the proxy chain.
For related Vue issues, see Fix: Vue v-model Not Working on Custom Components and Fix: Vue Computed Property Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Pinia Store Not Working — State Not Reactive, Actions Not Updating, or Store Not Found
How to fix Pinia store issues — state reactivity with storeToRefs, getters not updating, actions async patterns, store outside components, SSR hydration, and testing Pinia stores.
Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible
How to fix Vue 3 slot issues — v-slot syntax, named slots, scoped slots passing data, default slot content, fallback content, and dynamic slot names.
Fix: Vue Computed Property Not Updating — Reactivity Not Triggered
How to fix Vue computed properties not updating — reactive dependency tracking, accessing nested objects, computed setters, watchEffect vs computed, and Vue 3 reactivity pitfalls.
Fix: Vue v-model Not Working on Custom Components — Prop Not Syncing
How to fix Vue v-model on custom components — defineModel, modelValue/update:modelValue pattern, multiple v-model bindings, v-model modifiers, and Vue 2 vs Vue 3 differences.