Pinia state management
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:
- Avoid expensive computations in getters
- Use
$patch
for batch updates - Split stores appropriately to avoid overly large single stores
- 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.
- View all registered stores in DevTools
- Track state changes
- View getter cache status
- Manually trigger actions for testing
Migrating from Vuex to Pinia
For existing Vuex projects, you can migrate to Pinia incrementally:
- First, install Pinia and create basic configuration
- Start by migrating a simple store
- Gradually convert Vuex state to Pinia state
- Convert mutations to direct state modifications or actions
- Convert Vuex getters to Pinia getters
- 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
上一篇:Vue3项目脚手架
下一篇:VueUse组合式工具库