阿里云主机折上折
  • 微信号
Current Site:Index > Composition-style Store writing

Composition-style Store writing

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

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 refs 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

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 ☕.