Skip to main content
Web DevelopmentTutorials

State Management in Vue 3: Pinia Patterns and Pitfalls

Master Pinia with real-world patterns: store composition, async actions, optimistic updates, and when you don't need a store at all.

Mbeah Essilfie

Mbeah Essilfie

June 23, 2026 at 06:03 AM

9 min read 602 views
State Management in Vue 3: Pinia Patterns and Pitfalls

When Do You Need a Store?

Not every piece of state belongs in Pinia. Use stores for:

  • State shared across multiple components
  • State that persists across route changes
  • Complex state with derived computations

For local component state, ref() is enough.

Setup Store Pattern

// stores/posts.ts
export const usePostsStore = defineStore('posts', () => {
  const posts = ref<Post[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  const publishedPosts = computed(() =>
    posts.value.filter(p => p.status === 'published')
  )

  async function fetchPosts() {
    loading.value = true
    error.value = null
    try {
      const { data } = await $fetch('/api/posts')
      posts.value = data
    } catch (e) {
      error.value = (e as Error).message
    } finally {
      loading.value = false
    }
  }

  async function deletePost(id: string) {
    // Optimistic update
    const previousPosts = [...posts.value]
    posts.value = posts.value.filter(p => p.id !== id)

    try {
      await $fetch(`/api/posts/${id}`, { method: 'DELETE' })
    } catch (e) {
      // Rollback on failure
      posts.value = previousPosts
      throw e
    }
  }

  return { posts, loading, error, publishedPosts, fetchPosts, deletePost }
})

Store Composition

Stores can use other stores:

export const useDashboardStore = defineStore('dashboard', () => {
  const postsStore = usePostsStore()
  const authStore = useAuthStore()

  const myPosts = computed(() =>
    postsStore.posts.filter(p => p.authorId === authStore.user?.id)
  )

  return { myPosts }
})
Avoid circular store dependencies. If Store A imports Store B and Store B imports Store A, refactor the shared logic into a third store or composable.

Hydration in SSR (Nuxt)

Pinia handles SSR hydration automatically in Nuxt. But watch out for:

// ❌ This runs on every request in SSR — shared state between users!
const globalState = ref([])

// ✅ State inside defineStore is per-request in SSR
export const useStore = defineStore('safe', () => {
  const state = ref([]) // Fresh per request
  return { state }
})

When NOT to Use Pinia

  • Server state → Use useFetch / useAsyncData with caching
  • Form state → Keep it local with reactive()
  • URL state → Use route query params
  • UI state → Local refs in the component

Pinia is powerful, but the simplest solution that works is always the right one.

TypeScriptVue
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
982