Test the Store strategy
Understanding the Core Concepts of Store Strategy
Store strategy in Vue.js typically refers to state management solutions, particularly the usage patterns of libraries like Pinia or Vuex. The core goal of state management libraries is to address the issue of state sharing between components. When multiple components need to access the same data source, passing it directly through props can become cumbersome. Stores provide centralized storage, allowing any component to read or modify the state.
// Basic Pinia store example
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
State Initialization and Type Safety
When creating a store, type safety can significantly reduce runtime errors. In Pinia, you can define the state structure using interfaces. For complex applications, it's recommended to divide the state into modular stores, with each store responsible for state in a specific domain.
// Typed Pinia store
interface UserState {
name: string
age: number
permissions: string[]
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
age: 0,
permissions: []
}),
actions: {
setUser(user: Partial<UserState>) {
this.$patch(user)
}
}
})
State Persistence Practices
Browser refreshes can cause store state to be lost, necessitating persistence solutions. Common approaches include using localStorage
or sessionStorage
, with plugins like pinia-plugin-persistedstate
simplifying the process.
// Persistence configuration example
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// Store configuration
export const useAuthStore = defineStore('auth', {
state: () => ({
token: null
}),
persist: {
key: 'app-auth',
storage: window.localStorage
}
})
Handling Complex State Changes
When multiple operations need to modify the same state, encapsulate the business logic in actions. Avoid modifying store state directly in components, as this makes state changes difficult to track.
// Shopping cart store example
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
discount: 0
}),
actions: {
addItem(product) {
const existing = this.items.find(item => item.id === product.id)
if (existing) {
existing.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
this.calculateDiscount()
},
calculateDiscount() {
if (this.items.length > 5) {
this.discount = 0.1
}
}
}
})
Component-Store Interaction Patterns
Components can interact with stores in various ways. Computed properties are suitable for derived state, while watch
can execute side effects in response to state changes. For large applications, using map
helper functions is recommended to keep code clean.
<script setup>
import { useCounterStore } from './stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// Use storeToRefs to maintain reactivity
const { count } = storeToRefs(counter)
function handleClick() {
counter.increment()
}
</script>
<template>
<button @click="handleClick">
Count is: {{ count }}
</button>
</template>
Strategies for Testing Stores
Unit testing stores requires validating state changes and action behaviors. With Vitest or Jest, you can create store instances and directly call methods for assertions.
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from './counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
test('increment action', () => {
const counter = useCounterStore()
expect(counter.count).toBe(0)
counter.increment()
expect(counter.count).toBe(1)
})
})
Performance Optimization Techniques
Stores in large applications may contain extensive data, requiring optimization strategies. Use getters to cache computed results, implement pagination for large arrays, and consider shallow reactivity to reduce unnecessary dependency tracking.
export const useProductStore = defineStore('products', {
state: () => ({
allProducts: []
}),
getters: {
featuredProducts(state) {
// Cache results using computed properties
return state.allProducts.filter(p => p.featured)
}
},
actions: {
async loadProducts(page = 1) {
const response = await fetch(`/api/products?page=${page}`)
this.allProducts = [...this.allProducts, ...response.data]
}
}
})
Error Handling Patterns
Asynchronous operations in stores require unified error handling mechanisms. You can create a base store class to encapsulate error handling logic or use interceptors to manage API call errors.
// Store example with error handling
export const useApiStore = defineStore('api', {
state: () => ({
error: null,
loading: false
}),
actions: {
async callApi(fn) {
this.loading = true
this.error = null
try {
return await fn()
} catch (err) {
this.error = err.message
throw err
} finally {
this.loading = false
}
}
}
})
// Usage example
const apiStore = useApiStore()
const userStore = useUserStore()
apiStore.callApi(async () => {
await userStore.loadUser()
})
Plugin Development and Extension
Pinia's plugin system allows extending store functionality. Common plugins include persistence, logging, and time-travel debugging. Plugins can access store instances and intercept various operations.
// Simple logging plugin
function piniaLogger() {
return ({ store }) => {
store.$onAction(({ name, args, after, onError }) => {
console.log(`Action "${name}" called with`, args)
after(result => {
console.log(`Action "${name}" completed with`, result)
})
onError(error => {
console.warn(`Action "${name}" failed with`, error)
})
})
}
}
const pinia = createPinia()
pinia.use(piniaLogger())
Service Layer Integration Patterns
Separating API calls from store logic improves testability. Create independent service modules to handle data fetching, while stores focus solely on state management.
// API service module
export const UserService = {
async getProfile() {
const response = await fetch('/api/profile')
return response.json()
}
}
// Using services in stores
export const useUserStore = defineStore('user', {
actions: {
async loadProfile() {
this.profile = await UserService.getProfile()
}
}
})
Reactive State Design
When designing store state, consider the characteristics of the reactivity system. Avoid replacing entire state objects directly; use $patch
for batch updates. For nested objects, ensure reactivity is maintained using reactive
.
export const useNestedStore = defineStore('nested', {
state: () => ({
user: {
name: '',
address: {
city: '',
street: ''
}
}
}),
actions: {
updateAddress(address) {
// Correct approach - use $patch
this.$patch({
user: {
address: {
...this.user.address,
...address
}
}
})
// Incorrect approach - loses reactivity
// this.user.address = address
}
}
})
Cross-Store Communication Solutions
Multiple stores may need to share data or coordinate operations. This can be achieved by calling other stores within actions or creating a top-level store to coordinate child stores.
export const useOrderStore = defineStore('order', {
actions: {
async submitOrder() {
const cart = useCartStore()
const user = useUserStore()
if (!user.isLoggedIn) {
throw new Error('Login required')
}
const response = await OrderService.create({
items: cart.items,
userId: user.id
})
cart.clear()
return response
}
}
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:TypeScript深度集成
下一篇:性能优化建议