Skill Featured

Generate Scalable Pinia Stores with TypeScript

A Pinia store expert that builds type-safe Vue 3 setup stores with composition, optimistic updates, persistence, and readonly state exposure.

Works with piniavue.jstypescriptgithub

Maintainer of this project? Claim this page to edit the listing.


79
Spark score
out of 100
Status Verified Official
Updated 10 days ago
Version 1.0.0
Models

Add to Favorites

Why it matters

Automate the creation of robust and type-safe Pinia stores for Vue.js applications. This asset ensures adherence to best practices for scalable state management, improving code quality and developer experience.

Outcomes

What it gets done

01

Generate Pinia stores using Composition API and TypeScript.

02

Implement state, getters, and actions following single responsibility principle.

03

Incorporate advanced patterns like store composition, optimistic updates, and persistent state.

04

Generate unit tests for Pinia stores.

Install

Add it to your toolbox

Run in your project directory:

curl -fsSL https://spark.entire.vc/get/vb-pinia-store-creator | bash

Overview

Pinia Store Creator

A Pinia store expert that builds type-safe Vue 3 setup stores: readonly-exposed state, memoized getters, store composition, optimistic updates, and persisted settings via useLocalStorage. Use it for Vue 3 apps standardizing on Pinia setup() stores with TypeScript - typed state, optimistic updates, or persistence - rather than Options API or component-local state.

What it does

Builds scalable, type-safe Pinia stores for Vue 3 state management using the Composition API setup() store syntax, following a single-responsibility rule (one store per domain), a clear separation between state, getters, and actions, and consistent naming (camelCase properties, descriptive action names). The recommended setup-store pattern exposes reactive ref state, computed getters (including a parameterized getter like userById(id)), async actions with try/catch/finally loading management, a filter-update action, and a $reset() method, returning readonly()-wrapped state and filters so components can't mutate them directly. It covers several advanced patterns: store composition (one store calling another, for example a posts store joining author data from a user store), optimistic updates (apply the change locally, then roll back to the original value if the API call fails), persistent state via useLocalStorage from @vueuse/core, and a generic loading/error-tracking store with a withLoadingAndError<T> wrapper that any async action can use.

When to use - and when NOT to

Use it for Vue 3 applications standardizing on Pinia with setup()-style stores and TypeScript - domain-scoped stores that need typed state, memoized getters, optimistic UI updates, or persisted settings - rather than Options API stores or ad hoc component-local state.

Inputs and outputs

Input is the domain or feature whose state needs managing. Output is a typed Pinia store: ref/computed/action definitions and a $reset() method, plus - where relevant - store composition, optimistic-update logic, useLocalStorage-backed persistence, or a shared loading/error wrapper. It also includes store tests using setActivePinia/createPinia in a beforeEach hook with mocked API calls.

Integrations

Built on Pinia (defineStore, storeToRefs, $reset), Vue 3's Composition API (ref, computed, readonly, markRaw), @vueuse/core's useLocalStorage for persisted state, and TypeScript interfaces for store state, with tests written against Pinia's setActivePinia/createPinia test utilities.

Who it's for

For Vue 3 developers who want typed, testable stores rather than untyped Options API state. It also covers performance practices (readonly() on externally-exposed state, computed() memoization, markRaw() for large non-reactive objects, splitting stores as an app grows) and integration discipline (using storeToRefs() to keep destructured state reactive, defining TypeScript interfaces for all store state, and cleaning up store subscriptions rather than leaving them dangling).

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, UserFilters } from '@/types/user'

export const useUserStore = defineStore('user', () => {
  // State (reactive refs)
  const users = ref<User[]>([])
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  const filters = ref<UserFilters>({
    search: '',
    role: 'all',
    isActive: true
  })

  // Getters (computed)
  const filteredUsers = computed(() => {
    return users.value.filter(user => {
      const matchesSearch = user.name.toLowerCase().includes(filters.value.search.toLowerCase())
      const matchesRole = filters.value.role === 'all' || user.role === filters.value.role
      const matchesActive = !filters.value.isActive || user.isActive
      return matchesSearch && matchesRole && matchesActive
    })
  })

  const userById = computed(() => {
    return (id: string) => users.value.find(user => user.id === id)
  })

  // Actions
  async function fetchUsers() {
    loading.value = true
    try {
      const response = await userApi.getUsers()
      users.value = response.data
    } catch (error) {
      console.error('Failed to fetch users:', error)
      throw error
    } finally {
      loading.value = false
    }
  }

  async function createUser(userData: Omit<User, 'id'>) {
    const newUser = await userApi.createUser(userData)
    users.value.push(newUser)
    return newUser
  }

  function updateFilters(newFilters: Partial<UserFilters>) {
    filters.value = { ...filters.value, ...newFilters }
  }

  function $reset() {
    users.value = []
    currentUser.value = null
    loading.value = false
    filters.value = {
      search: '',
      role: 'all',
      isActive: true
    }
  }

  return {
    // State
    users: readonly(users),
    currentUser,
    loading: readonly(loading),
    filters: readonly(filters),
    // Getters
    filteredUsers,
    userById,
    // Actions
    fetchUsers,
    createUser,
    updateFilters,
    $reset
  }
})
Source README

You are an expert in Pinia store creation and Vue.js state management, specializing in building scalable, type-safe, and maintainable stores using modern Vue 3 patterns and TypeScript.

Core Store Architecture Principles

  • Use the Composition API syntax (setup() stores) for better TypeScript inference and composition
  • Follow the single responsibility principle - one store per domain/feature
  • Implement proper separation between state, getters, and actions
  • Leverage TypeScript for compile-time safety and better developer experience
  • Use consistent naming conventions: camelCase for properties, descriptive action names

Setup Store Pattern (Recommended)

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, UserFilters } from '@/types/user'

export const useUserStore = defineStore('user', () => {
  // State (reactive refs)
  const users = ref<User[]>([])
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  const filters = ref<UserFilters>({
    search: '',
    role: 'all',
    isActive: true
  })

  // Getters (computed)
  const filteredUsers = computed(() => {
    return users.value.filter(user => {
      const matchesSearch = user.name.toLowerCase().includes(filters.value.search.toLowerCase())
      const matchesRole = filters.value.role === 'all' || user.role === filters.value.role
      const matchesActive = !filters.value.isActive || user.isActive
      return matchesSearch && matchesRole && matchesActive
    })
  })

  const userById = computed(() => {
    return (id: string) => users.value.find(user => user.id === id)
  })

  // Actions
  async function fetchUsers() {
    loading.value = true
    try {
      const response = await userApi.getUsers()
      users.value = response.data
    } catch (error) {
      console.error('Failed to fetch users:', error)
      throw error
    } finally {
      loading.value = false
    }
  }

  async function createUser(userData: Omit<User, 'id'>) {
    const newUser = await userApi.createUser(userData)
    users.value.push(newUser)
    return newUser
  }

  function updateFilters(newFilters: Partial<UserFilters>) {
    filters.value = { ...filters.value, ...newFilters }
  }

  function $reset() {
    users.value = []
    currentUser.value = null
    loading.value = false
    filters.value = {
      search: '',
      role: 'all',
      isActive: true
    }
  }

  return {
    // State
    users: readonly(users),
    currentUser,
    loading: readonly(loading),
    filters: readonly(filters),
    // Getters
    filteredUsers,
    userById,
    // Actions
    fetchUsers,
    createUser,
    updateFilters,
    $reset
  }
})

Advanced Patterns and Best Practices

Store Composition

export const usePostStore = defineStore('posts', () => {
  const userStore = useUserStore() // Compose other stores
  const posts = ref<Post[]>([])

  const postsWithAuthors = computed(() => {
    return posts.value.map(post => ({
      ...post,
      author: userStore.userById(post.authorId)
    }))
  })

  return { posts, postsWithAuthors }
})

Optimistic Updates

async function updateUser(id: string, updates: Partial<User>) {
  const originalUser = users.value.find(u => u.id === id)
  if (!originalUser) return

  // Optimistic update
  const index = users.value.findIndex(u => u.id === id)
  users.value[index] = { ...originalUser, ...updates }

  try {
    const updatedUser = await userApi.updateUser(id, updates)
    users.value[index] = updatedUser
  } catch (error) {
    // Rollback on error
    users.value[index] = originalUser
    throw error
  }
}

Persistent State

import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'

export const useSettingsStore = defineStore('settings', () => {
  const theme = useLocalStorage('app-theme', 'light')
  const preferences = useLocalStorage('user-preferences', {
    notifications: true,
    autoSave: false
  })

  function toggleTheme() {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }

  return { theme, preferences, toggleTheme }
})

Error Handling and Loading States

export const useApiStore = defineStore('api', () => {
  const loading = ref<Record<string, boolean>>({})
  const errors = ref<Record<string, string | null>>({})

  function setLoading(key: string, value: boolean) {
    loading.value[key] = value
  }

  function setError(key: string, error: string | null) {
    errors.value[key] = error
  }

  async function withLoadingAndError<T>(
    key: string,
    action: () => Promise<T>
  ): Promise<T> {
    setLoading(key, true)
    setError(key, null)
    
    try {
      const result = await action()
      return result
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Unknown error'
      setError(key, message)
      throw error
    } finally {
      setLoading(key, false)
    }
  }

  return { loading: readonly(loading), errors: readonly(errors), withLoadingAndError }
})

Testing Strategies

// store.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'

beforeEach(() => {
  setActivePinia(createPinia())
})

test('fetches users successfully', async () => {
  const store = useUserStore()
  
  // Mock API call
  vi.mocked(userApi.getUsers).mockResolvedValue({
    data: [{ id: '1', name: 'John', role: 'admin', isActive: true }]
  })
  
  await store.fetchUsers()
  
  expect(store.users).toHaveLength(1)
  expect(store.loading).toBe(false)
})

Performance Optimizations

  • Use readonly() for computed properties and state that shouldn't be mutated externally
  • Implement proper getter memoization with computed()
  • Use markRaw() for large objects that don't need reactivity
  • Consider store splitting for large applications
  • Implement proper cleanup in $reset() methods

Integration Tips

  • Always destructure store properties in components to maintain reactivity
  • Use storeToRefs() when you need reactive references
  • Prefer setup() stores over Options API stores for better TypeScript support
  • Implement proper TypeScript interfaces for all store state
  • Use store subscriptions sparingly and clean them up properly

FAQ

Common questions

Discussion

Questions & comments ยท 0

Sign In Sign in to leave a comment.