阿里云主机折上折
  • 微信号
Current Site:Index > Pinia state management

Pinia state management

Author:Chuan Chen 阅读数:44466人阅读 分类: Vue.js

Core Concepts of Pinia

Pinia is a lightweight state management library for Vue.js that provides a simple and intuitive way to manage application state. Compared to Vuex, Pinia offers a more concise API and better TypeScript support. The core concepts of Pinia include store, state, getters, and actions.

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

Creating and Using a Store

A store in Pinia is an entity that contains state and business logic. To create a store, use the defineStore function. Each store has a unique ID for referencing it in components.

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: 'John Doe',
    age: 30,
    email: 'john@example.com'
  }),
  actions: {
    updateUser(partialUser) {
      this.$patch(partialUser)
    }
  }
})

Using the store in a component:

<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

<template>
  <div>
    <p>Name: {{ userStore.name }}</p>
    <p>Age: {{ userStore.age }}</p>
    <button @click="userStore.updateUser({ age: 31 })">
      Increment Age
    </button>
  </div>
</template>

State Management

The state in Pinia is the core part of a store, containing the application's data. The state must be a function that returns the initial state, ensuring each request gets an independent store instance.

export const useProductStore = defineStore('products', {
  state: () => ({
    items: [
      { id: 1, name: 'Laptop', price: 999 },
      { id: 2, name: 'Phone', price: 699 }
    ],
    loading: false,
    error: null
  })
})

You can modify the state directly, but it's recommended to use the $patch method for batch updates:

// Direct modification
productStore.items.push({ id: 3, name: 'Tablet', price: 399 })

// Using $patch
productStore.$patch({
  items: [...productStore.items, { id: 3, name: 'Tablet', price: 399 }],
  loading: false
})

Using Getters

Getters are computed properties of a store that derive new values based on the state. Getters are cached internally and only recomputed when their dependencies change.

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  getters: {
    totalItems: (state) => state.items.length,
    totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price, 0),
    hasItem: (state) => (productId) => state.items.some(item => item.id === productId)
  }
})

Using getters in a component:

<script setup>
import { useCartStore } from '@/stores/cart'

const cartStore = useCartStore()
</script>

<template>
  <div>
    <p>Total Items: {{ cartStore.totalItems }}</p>
    <p>Total Price: ${{ cartStore.totalPrice }}</p>
    <p v-if="cartStore.hasItem(1)">
      Product #1 is in your cart
    </p>
  </div>
</template>

Actions and Asynchronous Operations

Actions are equivalent to methods in components, used to encapsulate business logic. Actions can be asynchronous, making them ideal for handling API calls.

export const usePostStore = defineStore('posts', {
  state: () => ({
    posts: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchPosts() {
      this.loading = true
      this.error = null
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/posts')
        this.posts = await response.json()
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    async createPost(postData) {
      const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
        method: 'POST',
        body: JSON.stringify(postData)
      })
      this.posts.unshift(await response.json())
    }
  }
})

Calling an action in a component:

<script setup>
import { usePostStore } from '@/stores/post'
import { onMounted } from 'vue'

const postStore = usePostStore()

onMounted(() => {
  postStore.fetchPosts()
})

const handleSubmit = async () => {
  await postStore.createPost({
    title: 'New Post',
    body: 'This is a new post',
    userId: 1
  })
}
</script>

Modularity and Composing Stores

Pinia supports splitting application state into multiple stores, which can reference and compose with each other.

// stores/auth.js
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: null
  }),
  actions: {
    async login(credentials) {
      // Login logic
    }
  }
})

// stores/notification.js
export const useNotificationStore = defineStore('notification', {
  state: () => ({
    messages: []
  }),
  actions: {
    addMessage(message) {
      this.messages.push(message)
    }
  }
})

Using these stores in another store:

// stores/ui.js
export const useUiStore = defineStore('ui', {
  actions: {
    async performLogin(credentials) {
      const authStore = useAuthStore()
      const notificationStore = useNotificationStore()
      
      try {
        await authStore.login(credentials)
        notificationStore.addMessage('Login successful')
      } catch (error) {
        notificationStore.addMessage('Login failed')
      }
    }
  }
})

Plugins and Persistence

Pinia supports a plugin system to extend store functionality. A common use case is implementing state persistence.

// Create a simple persistence plugin
function persistPlugin({ store }) {
  const key = `pinia-${store.$id}`
  const savedState = localStorage.getItem(key)
  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }
  
  store.$subscribe((mutation, state) => {
    localStorage.setItem(key, JSON.stringify(state))
  })
}

// Use the plugin when creating a Pinia instance
import { createPinia } from 'pinia'

const pinia = createPinia()
pinia.use(persistPlugin)

For more complex persistence needs, use the pinia-plugin-persistedstate plugin:

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// Enable persistence in a store
export const useSettingsStore = defineStore('settings', {
  state: () => ({
    theme: 'light',
    language: 'en'
  }),
  persist: true
})

Integration with Vue Router and Composition API

Pinia integrates well with Vue Router and the Composition API to create powerful application architectures.

// Using a store in route guards
import { createRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  // Route configuration
})

router.beforeEach(async (to) => {
  const authStore = useAuthStore()
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    return '/login'
  }
})

Using a store in composable functions:

// composables/useUser.js
import { useUserStore } from '@/stores/user'
import { computed } from 'vue'

export function useUser() {
  const userStore = useUserStore()
  
  const fullName = computed(() => {
    return `${userStore.firstName} ${userStore.lastName}`
  })
  
  async function updateProfile(data) {
    await userStore.updateProfile(data)
  }
  
  return {
    user: userStore.user,
    fullName,
    updateProfile
  }
}

Testing Pinia Stores

Testing Pinia stores is straightforward using testing frameworks like Vitest or Jest.

import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { describe, it, expect, beforeEach } from 'vitest'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('increments count', () => {
    const counter = useCounterStore()
    expect(counter.count).toBe(0)
    counter.increment()
    expect(counter.count).toBe(1)
  })
  
  it('doubles count', () => {
    const counter = useCounterStore()
    counter.count = 5
    expect(counter.doubleCount).toBe(10)
  })
})

Performance Optimization and Best Practices

For optimal performance, follow these Pinia best practices:

  1. Avoid expensive computations in getters
  2. Use $patch for batch updates
  3. Split stores appropriately to avoid overly large single stores
  4. Use storeToRefs to maintain reactivity when destructuring stores
import { storeToRefs } from 'pinia'
import { useProductStore } from '@/stores/product'

const productStore = useProductStore()
// Incorrect way: loses reactivity
const { items, loading } = productStore

// Correct way: maintains reactivity
const { items, loading } = storeToRefs(productStore)

Integration with Vue DevTools

Pinia integrates well with Vue DevTools for easy debugging and state change tracking.

  1. View all registered stores in DevTools
  2. Track state changes
  3. View getter cache status
  4. Manually trigger actions for testing

Migrating from Vuex to Pinia

For existing Vuex projects, you can migrate to Pinia incrementally:

  1. First, install Pinia and create basic configuration
  2. Start by migrating a simple store
  3. Gradually convert Vuex state to Pinia state
  4. Convert mutations to direct state modifications or actions
  5. Convert Vuex getters to Pinia getters
  6. Update components to use the new Pinia store
// Vuex example
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    }
  }
})

// Converted to Pinia
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    incrementAsync() {
      setTimeout(() => {
        this.increment()
      }, 1000)
    }
  }
})

Handling Complex State Relationships

For states with complex relationships, use multiple stores and establish associations by referencing IDs.

// stores/authors.js
export const useAuthorStore = defineStore('authors', {
  state: () => ({
    authors: [
      { id: 1, name: 'Author One' },
      { id: 2, name: 'Author Two' }
    ]
  }),
  getters: {
    getAuthorById: (state) => (id) => state.authors.find(a => a.id === id)
  }
})

// stores/books.js
export const useBookStore = defineStore('books', {
  state: () => ({
    books: [
      { id: 1, title: 'Book One', authorId: 1 },
      { id: 2, title: 'Book Two', authorId: 2 }
    ]
  }),
  getters: {
    booksWithAuthors() {
      const authorStore = useAuthorStore()
      return this.books.map(book => ({
        ...book,
        author: authorStore.getAuthorById(book.authorId)
      }))
    }
  }
})

Server-Side Rendering (SSR) Support

Pinia fully supports server-side rendering and can be used in frameworks like Nuxt.js.

// Using Pinia in Nuxt.js
// nuxt.config.js
export default {
  modules: [
    '@pinia/nuxt'
  ]
}

// Then you can directly use stores in any component or page
export default defineComponent({
  setup() {
    const counter = useCounterStore()
    return { counter }
  }
})

For custom SSR setups, ensure each request has an independent store instance:

// In the server entry file
import { createPinia } from 'pinia'

export default (context) => {
  const pinia = createPinia()
  context.app.use(pinia)
}

TypeScript Support

Pinia provides excellent TypeScript support, allowing full typing of all parts of a store.

interface UserState {
  name: string
  age: number
  email: string
}

interface UserGetters {
  isAdult: boolean
}

interface UserActions {
  updateUser: (partialUser: Partial<UserState>) => void
}

export const useUserStore = defineStore<'user', UserState, UserGetters, UserActions>('user', {
  state: (): UserState => ({
    name: 'John Doe',
    age: 30,
    email: 'john@example.com'
  }),
  getters: {
    isAdult: (state) => state.age >= 18
  },
  actions: {
    updateUser(partialUser) {
      this.$patch(partialUser)
    }
  }
})

Advanced Patterns

Pinia supports advanced patterns like dynamic store creation and store composition.

// Dynamic store creation
function createDynamicStore(id, initialState) {
  return defineStore(id, {
    state: () => initialState,
    // Other options
  })
}

// Usage
const useDynamicStore = createDynamicStore('dynamic', { value: 0 })
const dynamicStore = useDynamicStore()

// Store composition
function createCommonStoreFeatures(id) {
  return {
    id,
    state: () => ({
      loading: false,
      error: null
    }),
    actions: {
      setLoading(loading) {
        this.loading = loading
      },
      setError(error) {
        this.error = error
      }
    }
  }
}

export const useEnhancedStore = defineStore('enhanced', {
  ...createCommonStoreFeatures('enhanced'),
  state: () => ({
    // Specific state
    data: null
  }),
  actions: {
    async fetchData() {
      this.setLoading(true)
      try {
        // Fetch data
        this.data = await fetchData()
      } catch (error) {
        this.setError(error)
      } finally {
        this.setLoading(false)
      }
    }
  }
})

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.