Skip to content

Fix: Pinia Store Not Working — State Not Reactive, Actions Not Updating, or Store Not Found

FixDevs ·

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 changes

Or 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 propertiesconst { name } = store extracts a plain value, breaking the reactive link. Use storeToRefs() 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. Calling useStore() at module level (outside components and composables) misses this.
  • $patch with 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 JS

Complete 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles