Fix: Pinia Store Not Working — State Not Reactive, Actions Not Updating, or Store Not Found
Quick Answer
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.
The Problem
Destructuring Pinia store state breaks reactivity:
const userStore = useUserStore();
// Reactive in template — works fine
// userStore.name updates correctly
// But destructuring loses reactivity
const { name, email } = userStore;
// name and email are plain strings — they don't update when store changesOr an action updates the store but the component doesn’t re-render:
// Store action updates state
actions: {
async fetchUser(id: string) {
const user = await api.getUser(id);
this.user = user; // Store updates but component stays stale
}
}Or useStore() called outside a component throws an error:
[🍍]: "getActivePinia()" was called but there was no active Pinia.
Are you trying to use a store before calling "app.use(pinia)"?Why This Happens
Pinia stores are built on Vue’s reactivity system. Losing reactivity usually comes from:
- Destructuring store properties —
const { name } = storeextracts a plain value, breaking the reactive link. UsestoreToRefs()to destructure while preserving reactivity. - Actions and methods are fine to destructure — only state and getters need
storeToRefs(). Actions can be destructured directly. - Store used before
app.use(pinia)— Pinia needs to be registered as a Vue plugin before any store is used. CallinguseStore()at module level (outside components and composables) misses this. $patchwith wrong data shape —$patch()does a shallow merge, not a deep merge. Nested objects need to be replaced entirely or use the function form.
Fix 1: Use storeToRefs for Reactive Destructuring
import { storeToRefs } from 'pinia';
import { useUserStore } from '@/stores/user';
const userStore = useUserStore();
// WRONG — breaks reactivity
const { name, email, isLoggedIn } = userStore;
// name is a plain string snapshot, won't update
// CORRECT — storeToRefs preserves reactivity for state and getters
const { name, email, isLoggedIn } = storeToRefs(userStore);
// name, email are Refs — template and computed() track changes
// Actions don't need storeToRefs — destructure directly
const { login, logout, fetchProfile } = userStore;
// In template or computed:
// {{ name }} works correctly — auto-unwraps the ref
// In script setup:
// console.log(name.value) — access .value in JSComplete example with Options API setup:
// UserProfile.vue
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useUserStore } from '@/stores/user';
const store = useUserStore();
// Reactive state and getters
const { name, email, avatar, fullName } = storeToRefs(store);
// Non-reactive (actions and methods)
const { updateProfile, logout } = store;
// Use in computed — reactive
const greeting = computed(() => `Hello, ${name.value}`);
</script>
<template>
<div>
<h1>{{ greeting }}</h1> <!-- Updates when name changes -->
<p>{{ email }}</p> <!-- Updates when email changes -->
<button @click="logout">Log out</button>
</div>
</template>Fix 2: Define Stores Correctly
Both Options API and Composition API store styles are supported:
// stores/user.ts — Options store style
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
loading: false,
error: null as string | null,
}),
getters: {
isLoggedIn: (state) => state.user !== null,
fullName: (state) => state.user
? `${state.user.firstName} ${state.user.lastName}`
: '',
},
actions: {
async fetchUser(id: string) {
this.loading = true;
this.error = null;
try {
this.user = await api.getUser(id);
} catch (e) {
this.error = e instanceof Error ? e.message : 'Unknown error';
} finally {
this.loading = false;
}
},
logout() {
this.user = null;
this.$reset(); // Reset all state to initial values
},
},
});// stores/cart.ts — Composition store style (more flexible)
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
// State (refs)
const items = ref<CartItem[]>([]);
const couponCode = ref('');
// Getters (computed)
const itemCount = computed(() => items.value.length);
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// Actions (functions)
function addItem(product: Product) {
const existing = items.value.find(i => i.id === product.id);
if (existing) {
existing.quantity++;
} else {
items.value.push({ ...product, quantity: 1 });
}
}
function removeItem(id: string) {
items.value = items.value.filter(i => i.id !== id);
}
async function checkout() {
const order = await api.createOrder({ items: items.value, coupon: couponCode.value });
items.value = [];
return order;
}
return { items, couponCode, itemCount, total, addItem, removeItem, checkout };
});Fix 3: Update State Correctly
Use $patch() or direct assignment — avoid .push() on non-reactive copies:
const store = useCartStore();
// Direct property assignment (reactive)
store.couponCode = 'SAVE10';
// $patch — merge update (shallow merge for objects)
store.$patch({
couponCode: 'SAVE10',
loading: false,
});
// $patch with function — for complex updates (recommended for arrays)
store.$patch((state) => {
state.items.push(newItem); // Safe — modifying state directly
state.items[0].quantity = 2; // Safe — reactive update
});
// WRONG — this replaces the reactive array reference
store.items = [...store.items, newItem]; // Works but triggers full replacement
// For Pinia, direct mutation inside $patch is preferred for arrays
// $reset — reset to initial state (Options API stores only)
store.$reset();Subscribe to store changes:
// Watch for state changes
store.$subscribe((mutation, state) => {
// mutation.type: 'direct' | 'patch object' | 'patch function'
// mutation.storeId: 'cart'
// state: the new state
// Persist to localStorage on every change
localStorage.setItem('cart', JSON.stringify(state));
});
// Watch a specific state value
watch(
() => store.itemCount,
(count) => console.log('Cart has', count, 'items')
);Fix 4: Use Stores Outside Components
Stores must be used after app.use(pinia). For utilities and services, pass the store or use a plugin:
// WRONG — called at module level, before Vue app is created
import { useUserStore } from '@/stores/user';
const store = useUserStore(); // Error: no active Pinia
// CORRECT — call useStore inside a function, not at module level
export async function protectedFetch(url: string) {
const userStore = useUserStore(); // Called when function runs, Vue is ready
const token = userStore.token;
return fetch(url, { headers: { Authorization: `Bearer ${token}` } });
}
// CORRECT — using the pinia instance directly
import { getActivePinia } from 'pinia';
export function getStoreOutsideComponent() {
const pinia = getActivePinia();
if (!pinia) throw new Error('Pinia not initialized');
// Access store with explicit pinia instance
const store = useUserStore(pinia);
return store;
}
// CORRECT — for router guards and other Vue hooks
// router/index.ts
router.beforeEach(async (to) => {
const userStore = useUserStore(); // Fine inside router guard — Vue is initialized
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
return { name: 'login' };
}
});Fix 5: Persist Store State
Use pinia-plugin-persistedstate to persist state across page reloads:
npm install pinia-plugin-persistedstate// main.ts
import { createPinia } from 'pinia';
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
app.use(pinia);// stores/user.ts — enable persistence
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
token: '',
preferences: { theme: 'light', language: 'en' },
}),
persist: {
// Persist only specific fields
pick: ['token', 'preferences'],
// Or persist everything
// persist: true,
// Custom storage (default: localStorage)
storage: sessionStorage,
// Custom key (default: store id)
key: 'my-app-user',
// Serialize/deserialize (default: JSON.stringify/parse)
serializer: {
deserialize: JSON.parse,
serialize: JSON.stringify,
},
},
});Fix 6: Test Pinia Stores
// stores/user.test.ts
import { setActivePinia, createPinia } from 'pinia';
import { useUserStore } from './user';
describe('useUserStore', () => {
beforeEach(() => {
// Create a fresh Pinia instance for each test
setActivePinia(createPinia());
});
it('initializes with correct defaults', () => {
const store = useUserStore();
expect(store.user).toBeNull();
expect(store.loading).toBe(false);
expect(store.isLoggedIn).toBe(false);
});
it('fetchUser updates state', async () => {
const store = useUserStore();
// Mock the API call
vi.spyOn(api, 'getUser').mockResolvedValueOnce({
id: '1',
firstName: 'Alice',
lastName: 'Smith',
email: '[email protected]',
});
await store.fetchUser('1');
expect(store.user?.firstName).toBe('Alice');
expect(store.isLoggedIn).toBe(true);
expect(store.loading).toBe(false);
});
it('handles fetch errors', async () => {
const store = useUserStore();
vi.spyOn(api, 'getUser').mockRejectedValueOnce(new Error('Network error'));
await store.fetchUser('1');
expect(store.user).toBeNull();
expect(store.error).toBe('Network error');
});
});Still Not Working?
Getters not updating — getters in Options API stores use (state) => ... and are automatically reactive. In Composition API stores, use computed(() => ...). If a getter depends on a ref that isn’t in the store state, it may not track correctly.
Store ID collision — each store must have a unique string ID (defineStore('user', ...)). If two stores use the same ID, they share the same instance. This is usually a bug — rename one of them.
HMR (Hot Module Replacement) issues — during development with Vite, hot reloading can cause Pinia stores to lose their state or get re-initialized. Use import.meta.hot to accept HMR updates properly, or just refresh the page when store changes aren’t reflecting correctly in dev.
For related Vue issues, see Fix: Vue Pinia State Not Reactive and Fix: Vue Composable Not Reactive.
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 State Not Reactive — Store Changes Not Updating the Component
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.
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.