Composition-style Store writing
Composable Store Pattern
The Composable Store is a way to manage state in Vue 3 using the Composition API. It leverages reactive APIs like ref
and reactive
to create reusable state logic, offering greater flexibility and modularity compared to the traditional Options API.
Basic Concepts
The core idea of a Composable Store is to organize related state and logic together into a reusable unit. Unlike state management libraries like Vuex, it doesn't require additional installation or configuration, directly utilizing Vue's reactivity system.
import { ref, computed } from 'vue'
export function useCounterStore() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return {
count,
doubleCount,
increment
}
}
Creating and Using a Store
Composable Stores are typically defined in separate files and then imported for use in components. This approach maintains a single source of truth for the state while avoiding global state pollution.
// stores/counter.js
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
Usage in a component:
<script setup>
import { useCounter } from './stores/counter'
const { count, increment } = useCounter()
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
State Sharing
When state needs to be shared across multiple components, reactive variables can be defined at the module level, ensuring all components importing the Store access the same state instance.
// stores/counter.js
import { ref } from 'vue'
const count = ref(0) // Shared state at module scope
export function useCounter() {
function increment() {
count.value++
}
return {
count,
increment
}
}
Complex State Management
For more complex state, reactive
can be used instead of multiple ref
s to keep related state within a single object.
// stores/user.js
import { reactive, computed } from 'vue'
const state = reactive({
user: null,
isAuthenticated: false
})
export function useUserStore() {
const fullName = computed(() => {
return state.user
? `${state.user.firstName} ${state.user.lastName}`
: 'Guest'
})
function login(userData) {
state.user = userData
state.isAuthenticated = true
}
function logout() {
state.user = null
state.isAuthenticated = false
}
return {
state,
fullName,
login,
logout
}
}
Handling Asynchronous Operations
Composable Stores can easily handle asynchronous operations like API requests. Combined with async/await
syntax, the code remains clear and readable.
// stores/posts.js
import { ref } from 'vue'
import api from '@/api'
const posts = ref([])
const isLoading = ref(false)
const error = ref(null)
export function usePostsStore() {
async function fetchPosts() {
isLoading.value = true
error.value = null
try {
const response = await api.get('/posts')
posts.value = response.data
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
return {
posts,
isLoading,
error,
fetchPosts
}
}
State Persistence
For state that needs to persist to localStorage or sessionStorage, corresponding logic can be added to the Store.
// stores/settings.js
import { ref, watch } from 'vue'
const settings = ref(
JSON.parse(localStorage.getItem('appSettings')) || {
theme: 'light',
fontSize: 16
}
)
watch(settings, (newValue) => {
localStorage.setItem('appSettings', JSON.stringify(newValue))
}, { deep: true })
export function useSettingsStore() {
function toggleTheme() {
settings.value.theme = settings.value.theme === 'light' ? 'dark' : 'light'
}
function increaseFontSize() {
settings.value.fontSize += 1
}
function decreaseFontSize() {
settings.value.fontSize = Math.max(12, settings.value.fontSize - 1)
}
return {
settings,
toggleTheme,
increaseFontSize,
decreaseFontSize
}
}
Modular Organization
As the application grows, Stores can be split by feature and then combined when needed.
// stores/index.js
import { useUserStore } from './user'
import { usePostsStore } from './posts'
import { useSettingsStore } from './settings'
export function useStore() {
return {
user: useUserStore(),
posts: usePostsStore(),
settings: useSettingsStore()
}
}
Usage in a component:
<script setup>
import { useStore } from './stores'
const { user, posts, settings } = useStore()
</script>
Type Safety (TypeScript)
TypeScript can provide better type support and code hints for Composable Stores.
// stores/counter.ts
import { ref } from 'vue'
interface CounterStore {
count: Ref<number>
increment: () => void
}
export function useCounter(): CounterStore {
const count = ref<number>(0)
function increment(): void {
count.value++
}
return {
count,
increment
}
}
Performance Optimization
For large-scale applications, shallowRef
or shallowReactive
can be used to reduce unnecessary reactivity overhead.
import { shallowRef } from 'vue'
const largeData = shallowRef({ /* Large data object */ })
Testing Strategy
Testing Composable Stores is relatively straightforward since they are pure JavaScript functions and don't depend on Vue component instances.
// stores/counter.spec.js
import { useCounter } from './counter'
describe('useCounter', () => {
it('should increment count', () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
})
Comparison with Pinia
While Composable Stores provide a lightweight state management solution, Pinia may be a better choice for more complex needs. Pinia is based on similar principles but offers additional features like DevTools integration and a plugin system.
// Pinia usage example
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
Practical Use Cases
Composable Stores are particularly suitable for small to medium-sized applications or local state management within larger applications, such as form handling or UI state management.
// stores/form.js
import { reactive, computed } from 'vue'
export function useFormStore(initialData) {
const form = reactive({ ...initialData })
const errors = reactive({})
const isValid = computed(() => Object.keys(errors).length === 0)
function validate() {
// Validation logic
}
function reset() {
Object.assign(form, initialData)
Object.keys(errors).forEach(key => delete errors[key])
}
return {
form,
errors,
isValid,
validate,
reset
}
}
Reactive Utility Functions
Vue provides a series of reactive utility functions that can be flexibly used in Composable Stores.
import { toRef, toRefs, isRef, unref } from 'vue'
export function useProductStore(product) {
// Convert reactive object properties to refs
const { id, name } = toRefs(product)
// Check if it's a ref
if (isRef(product.price)) {
// ...
}
// Get the value of a ref
const price = unref(product.price)
return {
id,
name,
price
}
}
Reusing Composable Stores
By designing function parameters and return values, Stores can be made more flexible and configurable.
// stores/pagination.js
import { ref, computed } from 'vue'
export function usePagination(totalItems, itemsPerPage = 10) {
const currentPage = ref(1)
const totalPages = computed(() =>
Math.ceil(totalItems / itemsPerPage)
)
function nextPage() {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
function prevPage() {
if (currentPage.value > 1) {
currentPage.value--
}
}
return {
currentPage,
totalPages,
nextPage,
prevPage
}
}
Side Effect Management
Using watch
and watchEffect
, side effects can be managed within Stores, keeping logic centralized.
import { ref, watch } from 'vue'
export function useSearchStore() {
const query = ref('')
const results = ref([])
watch(query, async (newQuery) => {
if (newQuery.trim()) {
results.value = await searchApi(newQuery)
} else {
results.value = []
}
}, { immediate: true })
return {
query,
results
}
}
State Reset
In some scenarios, the ability to reset state to its initial values is needed.
// stores/filters.js
import { reactive, toRaw } from 'vue'
export function useFiltersStore(initialFilters) {
const filters = reactive({ ...initialFilters })
const initial = toRaw(initialFilters) // Get non-reactive copy
function reset() {
Object.assign(filters, initial)
}
return {
filters,
reset
}
}
Cross-Store Communication
Although Composable Stores are typically independent, sometimes they need to communicate with each other.
// stores/auth.js
import { useUserStore } from './user'
export function useAuthStore() {
const { state: userState } = useUserStore()
function checkPermission(permission) {
return userState.user?.permissions.includes(permission)
}
return {
checkPermission
}
}
Reactive State Transformation
Using computed
, derived state can be created based on other states.
import { ref, computed } from 'vue'
export function useCartStore() {
const items = ref([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const itemCount = computed(() =>
items.value.reduce((count, item) => count + item.quantity, 0)
)
return {
items,
total,
itemCount
}
}
State Snapshots
Sometimes an immutable snapshot of the state is needed, which can be obtained using toRaw
or the spread operator.
import { reactive, toRaw } from 'vue'
export function useEditorStore() {
const state = reactive({
content: '',
selection: null
})
function getSnapshot() {
return { ...toRaw(state) }
}
return {
state,
getSnapshot
}
}
Performance-Sensitive Scenarios
For performance-sensitive scenarios, the granularity of reactive updates can be controlled.
import { shallowReactive, markRaw } from 'vue'
export function useCanvasStore() {
const shapes = shallowReactive([])
function addShape(shape) {
shapes.push(markRaw(shape)) // Mark as non-reactive
}
return {
shapes,
addShape
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:创建和使用Store
下一篇:Options式Store写法