Using useStore in the Vue 3 Composition API

In the Vue 3 ecosystem, the advent of the Composition API marks a significant paradigm shift in framework development. It enables developers to organize logic more flexibly, decoupling business rules from their components. For scenarios requiring complex data flow management and global shared state, traditional data/props often prove insufficient, making the adoption of a state management library a necessity. This article delves into integrating useStore (using Pinia as an example) within the Vue 3 Composition API, demonstrating its advantages over Vuex and best practices.
In the Vue 2 era, Vuex was the de facto standard, but its syntax was cumbersome and relied on specific computed property patterns. The Vue 3 official team announced it would no longer maintain Vuex, instead recommending Pinia (Pinata) as the next-generation state management library. Pinia is designed specifically for the Composition API and offers the following core advantages:

  • Simpler Syntax: It abandons complex mapping functions like mapState and computed, achieving logic reuse directly through property access and composition function calls.
  • Better TypeScript Support: It natively provides powerful type inference capabilities, reducing runtime errors.
  • Lightweight Design: It does not force dependencies on Vue APIs (though it remains compatible) and results in a smaller bundle size.

Initializing the Pinia Store

Before using useStore, you must first create or load a Pinia store instance. This is typically a singleton operation performed at application startup. Assume we have a file named userStore.js for managing user information:

// stores/userStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State definition
  const username = ref(null)
  const isLoggedIn = ref(false)
  
  // Computed property: Derive user info based on login status
  const userInfo = computed(() => {
    return { name: username.value, status: isLoggedIn.value ? 'active' : 'guest' }
  })

  // Actions (methods)
  function login(name) {
    if (!name) console.error('Username cannot be empty')
    username.value = name
    isLoggedIn.value = true
    
    // Example of state update simulation after an asynchronous operation
    setTimeout(() => {
      console.log(`User ${name} logged in successfully`)
    }, 1000)
  }

  function logout() {
    username.value = null
    isLoggedIn.value = false
  }

  return { username, isLoggedIn, userInfo, login, logout }
})

Using useStore in Components

Once the store is defined, you can retrieve the instance using useUserStore in any Vue 3 composables component. This approach supports both synchronous state reading and perfectly adapts to asynchronous operations and side effects.

Basic Usage: State Access and Updates

<!-- components/UserPanel.vue -->
<script setup>
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/userStore.js'

const store = useUserStore()

// Directly read computed properties, automatically triggering reactive updates
watchEffect(() => {
  console.log('Current user status:', store.userInfo)
})

onMounted(async () => {
  // Simulate fetching data from an API and logging in
  await new Promise(resolve => setTimeout(resolve, 500))
  
  const targetUser = 'Alice'
  store.login(targetUser)
  
  console.log('Status after login:', store.userInfo)
})
</script>

<template>
  <div class="user-panel">
    <p v-if="store.isLoggedIn">Welcome, {{ store.username }}</p>
    <button @click="$emit('logout')">Logout</button>
  </div>
</template>

Handling Asynchronous Side Effects: onMounted vs watchEffect

In the Composition API, state changes often accompany asynchronous operations like network requests or file reading. Pinia provides flexible mechanisms to handle these scenarios:

  1. Using useStore(): Directly inject the store instance, suitable for components that need to frequently call methods or access deep state.
  2. Combining with Side Effect Functions: Utilize onMounted, watchEffect, or perform asynchronous processing inside actions (e.g., using Promises) to ensure data flow consistency.

For example, when fetching a list of users, you can initiate a request within the action and return the result for multiple components to subscribe to:

// stores/userStore.js (Supplementary part)
function fetchUsers() {
  return new Promise((resolve, reject) => {
    // Simulate API call
    setTimeout(() => resolve(['User1', 'User2']), 1000)
  })
}

Best Practices and Considerations

To build robust applications, developers should keep the following points in mind:

  • Avoid Circular Dependencies: Although Pinia supports module references between stores, you should avoid scenarios where Store A references Store B which in turn references Store A. If this is unavoidable, consider using createNamespace or decoupling business logic.
  • Type Safety Configuration: Enable TypeScript checking in the development environment (.tsconfig.json) to ensure all state definitions and method parameters have explicit type annotations.
  • Production Optimization: Utilize Pinia's persistence plugins (e.g., vue-persistedstate) to automatically save sensitive data, preventing loss of user information after refreshing the page.

Summary

By combining Vue 3 Composition API with a state management library through useStore, developers can enjoy clearer code structure, stronger type inference, and better performance. This pattern not only addresses the limitations of traditional reactive systems when handling complex logic but also provides a solid technical foundation for building large Single Page Applications (SPAs). As the ecosystem matures, mastering this skill will be a mandatory requirement for every Vue 3 developer.