前端状态管理实战:Zustand 与 Pinia 对比

组件之间传 props 一层层嵌套,状态散落在各个组件里,改一个地方牵一发动全身。这就是状态管理要解决的问题。本文从状态管理的核心思路讲起,对比 React 生态的 Zustand 和 Vue 生态的 Pinia,帮你选对工具。

状态管理配图:蓝色渐变背景上中央 store 盒连接四个组件节点,数据流线,扁平科技插画
状态管理:把共享数据放到单一 store,组件直接订阅

一、为什么需要状态管理

想象一个电商页面:购物车组件、导航栏组件、商品详情组件都要知道当前购物车有多少件商品。如果用 props 一层层传,从 App 传到 Header 传到 CartBadge,中间经过 5 个不需要这个数据的组件,这就是 prop drilling。状态管理把共享数据放到一个全局 store,任何组件都能直接读写,不用层层传递。

// 没有状态管理:props 层层传递
// App.jsx
<App cart={cart}>
  <Header cart={cart} />
</App>

// Header.jsx 并不需要 cart,只是转发给 CartBadge
<Header cart={cart}>
  <CartBadge cart={cart} />
</Header>
判断要不要用状态管理:小项目用 React 的 useState/useContext 就够了。当你发现多个不相关组件共享同一份数据、或者组件嵌套超过 3 层还在传同一个 prop,就该上状态管理了。

二、Zustand:React 的轻量选择

Zustand 是 React 生态最流行的状态管理库,API 极简,不到 1KB。

import { create } from 'zustand'

// 创建 store
const useCartStore = create((set) => ({
  items: [],
  addItem: (product) => set((state) => ({
    items: [...state.items, product]
  })),
  removeItem: (id) => set((state) => ({
    items: state.items.filter(item => item.id !== id)
  })),
  clear: () => set({ items: [] })
}))

// 在组件中使用
function CartBadge() {
  const itemCount = useCartStore((state) => state.items.length)
  return <span>购物车: {itemCount} 件</span>
}

function ProductList() {
  const addItem = useCartStore((state) => state.addItem)
  return <button onClick={() => addItem({ id: 1, name: '键盘' })}>加入购物车</button>
}

Zustand 的核心是:一个 hook 就是一个 store。组件用 useCartStore(selector) 选择需要的状态,只有选中的状态变化才会重新渲染。

三、Pinia:Vue 的官方方案

Pinia 是 Vue 3 的官方状态管理库,替代了 Vuex。

// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  actions: {
    addItem(product) {
      this.items.push(product)
    },
    removeItem(id) {
      this.items = this.items.filter(item => item.id !== id)
    },
    clear() {
      this.items = []
    }
  },
  getters: {
    itemCount: (state) => state.items.length
  }
})

// 在组件中使用
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>

<template>
  <span>购物车: {{ cart.itemCount }} 件</span>
  <button @click="cart.addItem({ id: 1, name: '鼠标' })">加入购物车</button>
</template>

四、两者对比

维度ZustandPinia
生态ReactVue(也支持 React)
大小~1KB~1.5KB
API 风格钩子函数选项式 / 组合式
TypeScript原生支持好原生支持好
DevTools支持Vue DevTools 集成

五、异步处理

实际项目里 store 里经常要调 API。两个库处理异步都很自然。

// Zustand 异步
const useUserStore = create((set) => ({
  user: null,
  loading: false,
  fetchUser: async (id) => {
    set({ loading: true })
    const res = await fetch(`/api/users/${id}`)
    const data = await res.json()
    set({ user: data, loading: false })
  }
}))

// Pinia 异步
export const useUserStore = defineStore('user', {
  state: () => ({ user: null, loading: false }),
  actions: {
    async fetchUser(id) {
      this.loading = true
      const res = await fetch(`/api/users/${id}`)
      this.user = await res.json()
      this.loading = false
    }
  }
})

六、持久化与最佳实践

刷新页面后购物车数据就没了?加个持久化中间件。

// Zustand 持久化
import { persist } from 'zustand/middleware'

const useCartStore = create(
  persist(
    (set) => ({ items: [] }),
    { name: 'cart-storage' }  // 存到 localStorage
  )
)

// 最佳实践:按功能拆分 store
// stores/user.js     用户状态
// stores/cart.js     购物车状态
// stores/settings.js  设置状态
// 不要把所有东西塞到一个巨大的 store 里

七、总结

状态管理的本质是把共享数据从组件树里抽出来放到一个单一数据源。React 项目选 Zustand,Vue 项目选 Pinia。它们都很小、很简单,核心思路一致:store 存状态、组件订阅状态、action 修改状态。记住:不是所有项目都需要状态管理,先用组件内的 useState 解决,解决不了再上全局 store。