404 Not Found

404 Not Found


nginx

State Management Pinia

After Alice adds an item to her cart, the cart is empty when she navigates to another page—each page has its own component state. Bob notices that the user's login status is inconsistent between SSR and the client-side. Charlie needs Pinia to manage shared state across components and pages.

1. What You'll Learn


2. A True Story from a Consumer

(1) Pain Point: "Lost" Shopping Cart Status

Alice added three items to her cart on the MegaShop homepage, but when she clicked to view the product details page, the cart icon showed 0 items—the status wasn't synced. Bob encountered a similar issue: after logging in and refreshing the page, his logged-in status disappeared.

(2) Solutions for Pinia State Management

Pinia decouples the shopping cart and user state from the component lifecycle, turning them into globally shared, reactive data:

TYPESCRIPT
// composables/useCartStore.ts
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price * i.quantity, 0))
  return { items, total }
})

(3) Benefit: Cross-page state persistence

Alice's shopping cart remains consistent across all pages, and Bob's login status is now synchronized between SSR and the client, eliminating any issues with state loss.


3. Pinia Installation and Configuration

(1) Installation Steps

BASH
npm install @pinia/nuxt pinia

(1) ▶ Example: nuxt.config.ts configuration

TYPESCRIPT
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  // Pinia is auto-configured, no extra setup needed
})

Output:

TEXT
// Execution Successful

(2) Comparison of Pinia, Vuex, and useState

Dimension Pinia Vuex 4 Nuxt useState
Vue 3 Support ✅ Native ⚠️ Compatibility Mode ✅ Nuxt-Exclusive
TypeScript ✅ Full type inference ❌ Manual declaration required ✅ Generic support
SSR Hydration ✅ Automatic ⚠️ Requires configuration ✅ Automatic
Code Splitting ✅ On-demand ❌ Global ❌ Global
DevTools ✅ Supported ✅ Supported ❌ Not supported
Size ~1 KB ~6 KB Built-in
Use Cases Complex Scenarios Legacy Migration Simple Sharing

4. How to Define a Store

(1) Option-Based Store

(1) ▶ Example: Option-Based Shopping Cart Store

TYPESCRIPT
// stores/cart.ts
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[],
    couponCode: '' as string
  }),

  getters: {
    totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),

    totalPrice: (state) => {
      return state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    },

    formattedTotal(): string {
      return new Intl.NumberFormat('en-US', {
        style: 'currency', currency: 'USD'
      }).format(this.totalPrice)
    }
  },

  actions: {
    addItem(product: Product) {
      const existing = this.items.find(i => i.id === product.id)
      if (existing) {
        existing.quantity++
      } else {
        this.items.push({ ...product, quantity: 1 })
      }
    },

    removeItem(productId: number) {
      this.items = this.items.filter(i => i.id !== productId)
    },

    updateQuantity(productId: number, quantity: number) {
      const item = this.items.find(i => i.id === productId)
      if (item) item.quantity = Math.max(0, quantity)
      this.items = this.items.filter(i => i.quantity > 0)
    },

    clearCart() {
      this.items = []
      this.couponCode = ''
    }
  }
})

interface CartItem {
  id: number; name: string; price: number; quantity: number; image: string
}
interface Product {
  id: number; name: string; price: number; image: string
}

Output:

TEXT
// Execution Successful

(2) Setup Syntax Store

(2) ▶ Example: Setup Syntax User Store

TYPESCRIPT
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const isAuthenticated = computed(() => !!user.value)
  const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '')

  // Actions
  async function login(email: string, password: string) {
    const response = await $fetch('/api/auth/login', {
      method: 'POST',
      body: { email, password }
    })
    user.value = response.user
  }

  function logout() {
    user.value = null
  }

  async function fetchProfile() {
    const profile = await $fetch('/api/user/profile')
    user.value = profile
  }

  return {
    user, isAuthenticated, fullName,
    login, logout, fetchProfile
  }
})

interface User {
  id: number; email: string; firstName: string; lastName: string; role: 'customer' | 'admin'
}

Output:

TEXT
// Execution Successful

(3) Comparison of the Two Methods

Dimension Option Type Setup Syntax
Syntax state/getters/actions ref/computed/function
TypeScript ⚠️ Interface declaration required ✅ Automatic inference
Flexibility ⚠️ Limited ✅ Available in any Composable
SSR ✅ Supports $reset ⚠️ Does not support $reset (must be implemented manually)
Use Cases Simple Store Complex Store/Requires Composable

5. SSR Dehydration and Hydration

(1) Pinia SSR State Transitions

100%
flowchart LR
    A[Server: Store filled with data] --> B[Serialize state to HTML payload]
    B --> C[Client: Read payload]
    C --> D[Hydrate Store with server state]
    D --> E[Client: Store ready, no re-fetch]

(1) ▶ Example: Initializing the Store in SSR

TYPESCRIPT
// plugins/init-pinia.server.ts
export default defineNuxtPlugin(() => {
  const cartStore = useCartStore()
  // Initialize cart from cookie on server
  const cartCookie = useCookie('cart-items')
  if (cartCookie.value) {
    cartStore.items = cartCookie.value
  }
})

Output:

TEXT
// Execution Successful

(2) ▶ Example: Differences Between Pinia and useState for SSR

Dimension Pinia useState
SSR Hydration ✅ Automatic payload ✅ Automatic payload
Multi-Store ✅ Independent Namespaces ⚠️ Manual Key Management
DevTools ✅ Visualization ❌ None
Persistence ✅ Plugin Support ⚠️ Manual cookie required
Complex logic ✅ actions/getters ❌ ref only

6. StoreToRefs and Batch Operations

(1) ▶ Example: StoreToRefs Reactive Destructuring

VUE
<script setup lang="ts">
const cartStore = useCartStore()

// ✅ Reactive destructure - keeps reactivity
const { items, totalPrice, totalItems } = storeToRefs(cartStore)

// ❌ Direct destructure - loses reactivity
// const { items, totalPrice } = cartStore

// Actions can be destructured directly (no reactivity needed)
const { addItem, removeItem, clearCart } = cartStore
</script>

Output:

TEXT
// Execution Successful

(2) ▶ Example: $patch batch update

TYPESCRIPT
// Batch update with $patch - single reactivity trigger
const cartStore = useCartStore()

// Object style
cartStore.$patch({
  couponCode: 'SAVE20',
  items: [...cartStore.items, newItem]
})

// Function style (better for array mutations)
cartStore.$patch((state) => {
  state.couponCode = 'SAVE20'
  state.items.push(newItem)
  state.items[0].quantity = 3
})

Output:

TEXT
// Execution Successful

(3) ▶ Example: Shopping Cart Persistence Plugin

TYPESCRIPT
// plugins/pinia-persist.client.ts
export default defineNuxtPlugin(({ $pinia }) => {
  $pinia.use(({ store }) => {
    // Load from localStorage on client
    const saved = localStorage.getItem(`pinia-${store.$id}`)
    if (saved) store.$patch(JSON.parse(saved))

    // Save to localStorage on change
    store.$subscribe((mutation, state) => {
      localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(state))
    })
  })
})

Output:

TEXT
// Execution Successful

7. Comprehensive Example: MegaShop Shopping Cart System

VUE
<!-- pages/cart.vue -->
<template>
  <div class="cart-page">
    <h1>Shopping Cart</h1>

    <div v-if="items.length === 0" class="empty-cart">
      <p>Your cart is empty</p>
      <NuxtLink to="/products">Continue Shopping</NuxtLink>
    </div>

    <div v-else>
      <div class="cart-items">
        <div v-for="item in items" :key="item.id" class="cart-item">
          <img :src="item.image" :alt="item.name" />
          <div class="details">
            <h3>{{ item.name }}</h3>
            <p>${{ item.price }} USD</p>
            <div class="quantity">
              <button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
              <span>{{ item.quantity }}</span>
              <button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
            </div>
          </div>
          <button @click="removeItem(item.id)" class="remove">Remove</button>
        </div>
      </div>

      <div class="cart-summary">
        <p>Items: {{ totalItems }}</p>
        <p class="total">Total: {{ formattedTotal }}</p>
        <button @click="clearCart" class="clear">Clear Cart</button>
        <NuxtLink to="/checkout" class="checkout">Proceed to Checkout</NuxtLink>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
const cartStore = useCartStore()
const { items, totalItems, formattedTotal } = storeToRefs(cartStore)
const { removeItem, updateQuantity, clearCart } = cartStore
</script>

❓ FAQ

Q How do I choose between Pinia Store and Composable?
A Use Store when you need to share state across components, and use Composable to reuse logic within a component. Use Store for shopping cart and user state, and use Composable for price formatting.
Q Why does deconstructing a Store cause it to lose its reactivity?
A Directly deconstructing a Store returns a copy of the value, not a ref. Use storeToRefs to deconstruct while maintaining reactivity; actions can be deconstructed directly (functions do not require reactivity).
Q What should I do if there are inconsistencies in Store data between the client and server during SSR?
A Make sure not to modify the Store before onMounted (values modified during the SSR phase will be overwritten during hydration). Pinia's SSR hydration is automatic and typically does not require manual handling.
Q Is Pinia's $reset not available in the Setup syntax?
A The Setup syntax does not include $reset. You can implement this manually by defining a $reset action or resetting all refs in $patch.
Q Will modifying the Store on multiple pages simultaneously cause conflicts?
A The client is single-threaded, so there will be no conflicts. During SSR, each request has its own independent Pinia instance, so there will be no conflicts either.
Q How can shopping cart data be retained after a page refresh?
A Use a persistence plugin (storing data in localStorage) or sync it to a cookie. MegaShop recommends using cookies for synchronization, as they can be read even during server-side rendering (SSR).

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty: ⭐): Create useCartStore, implement addItem and removeItem, manipulate the shopping cart on two different pages, and verify that the state is shared.
  2. Advanced Exercise (Difficulty: ⭐⭐): Use the Setup syntax to define useUserStore, implement login, logout, and fetchProfile, and verify that SSR hydration works correctly.
  3. Challenge (Difficulty: ⭐⭐⭐): Implement shopping cart persistence (so data is retained after refreshing the page), and compare the pros and cons of using the localStorage plugin versus cookies for synchronization.

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏