教程 zustand tutorial

非 React 组件中使用 Store

Zustand 虽然主要设计用于 React 应用,但它也提供了 “vanilla”(纯 JavaScript)模式,允许你在非 React 环境中使用 Zustand Store。这对于混合应用、旧项目迁移或需要在多个框架间共享状态的场景非常有用。

Vanilla 模式的基本概念

在 vanilla 模式下,Zustand 提供了一个独立的 Store 实例,不依赖于 React 的上下文或钩子。你可以:

  • 创建独立的 Store 实例
  • 直接访问和更新状态
  • 订阅状态变化
  • 使用所有 Zustand 中间件

创建 Vanilla Store

安装

首先确保已经安装了 Zustand:

bash
npm install zustand

基本创建方式

使用 createStore 函数创建 vanilla Store:

javascript
import { createStore } from 'zustand/vanilla'

// 创建一个简单的计数器 Store
const counterStore = createStore((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  incrementBy: (value) => set((state) => ({ count: state.count + value })),
  getCountDouble: () => get().count * 2,
}))

访问和更新状态

获取当前状态

javascript
// 获取当前状态
const currentState = counterStore.getState()
console.log(currentState.count) // 输出: 0

更新状态

javascript
// 更新状态
counterStore.getState().increment()
console.log(counterStore.getState().count) // 输出: 1

counterStore.getState().incrementBy(5)
console.log(counterStore.getState().count) // 输出: 6

counterStore.getState().reset()
console.log(counterStore.getState().count) // 输出: 0

直接调用 set 函数

你也可以直接使用 Store 实例的 set 方法更新状态:

javascript
// 直接使用 set 方法
counterStore.setState((state) => ({ count: state.count + 1 }))
console.log(counterStore.getState().count) // 输出: 1

订阅状态变化

基本订阅

使用 subscribe 方法监听状态变化:

javascript
// 订阅所有状态变化
const unsubscribe = counterStore.subscribe((newState, oldState) => {
  console.log('状态变化:', { oldState, newState })
})

// 触发状态变化
counterStore.getState().increment()
// 输出: 状态变化: { oldState: { count: 0 }, newState: { count: 1 } }

选择性订阅

你可以通过 Selector 函数只订阅感兴趣的状态部分:

javascript
// 只订阅 count 的变化
const unsubscribeCount = counterStore.subscribe(
  (state) => state.count, // Selector 函数
  (newCount, oldCount) => {
    console.log('count 变化:', { oldCount, newCount })
  }
)

counterStore.getState().increment()
// 输出: count 变化: { oldCount: 1, newCount: 2 }

浅比较订阅

使用 subscribeWithSelector 中间件支持浅比较:

javascript
import { createStore } from 'zustand/vanilla'
import { subscribeWithSelector } from 'zustand/middleware'

const userStore = createStore(
  subscribeWithSelector((set) => ({
    user: {
      name: '张三',
      age: 30,
    },
    updateName: (name) => set((state) => ({ user: { ...state.user, name } })),
    updateAge: (age) => set((state) => ({ user: { ...state.user, age } })),
  }))
)

// 只订阅 user 对象,使用浅比较
const unsubscribeUser = userStore.subscribe(
  (state) => state.user,
  (newUser, oldUser) => {
    console.log('user 变化:', { oldUser, newUser })
  },
  { equalityFn: (a, b) => a.name === b.name && a.age === b.age } // 浅比较
)

userStore.getState().updateName('李四')
// 输出: user 变化: { oldUser: { name: '张三', age: 30 }, newUser: { name: '李四', age: 30 } }

取消订阅

基本取消订阅

调用订阅函数返回的 unsubscribe 函数:

javascript
const unsubscribe = counterStore.subscribe((newState) => {
  console.log('状态变化:', newState)
})

// 取消订阅
unsubscribe()

// 后续的状态变化将不再触发回调
counterStore.getState().increment() // 不会输出任何内容

取消所有订阅

使用 destroy 方法取消所有订阅并清理 Store:

javascript
// 取消所有订阅并清理 Store
counterStore.destroy()

与中间件一起使用

Vanilla Store 支持所有 Zustand 中间件,如 persist、immer 和 devtools。

Persist 中间件

javascript
import { createStore } from 'zustand/vanilla'
import { persist } from 'zustand/middleware'

const counterStore = createStore(
  persist(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'counter-storage',
    }
  )
)

Immer 中间件

javascript
import { createStore } from 'zustand/vanilla'
import { immer } from 'zustand/middleware/immer'

const todoStore = createStore(
  immer((set) => ({
    todos: [],
    addTodo: (text) =>
      set((state) => {
        state.todos.push({ id: Date.now(), text, completed: false })
      }),
    toggleTodo: (id) =>
      set((state) => {
        const todo = state.todos.find((todo) => todo.id === id)
        if (todo) todo.completed = !todo.completed
      }),
  }))
)

DevTools 中间件

javascript
import { createStore } from 'zustand/vanilla'
import { devtools } from 'zustand/middleware'

const counterStore = createStore(
  devtools(
    (set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
    }),
    {
      name: 'counter-store',
    }
  )
)

与 React 组件结合使用

你可以在同一个项目中同时使用 vanilla Store 和 React Store,甚至将 vanilla Store 转换为 React Hook:

将 Vanilla Store 转换为 React Hook

javascript
import { create } from 'zustand'
import { counterStore } from './counterStore' // 导入 vanilla Store

// 将 vanilla Store 转换为 React Hook
const useCounterStore = create(() => counterStore.getState())

// 在 React 组件中使用
function Counter() {
  const { count, increment } = useCounterStore()
  
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  )
}

在 React 和非 React 之间共享状态

javascript
// store.js - 共享的 vanilla Store
import { createStore } from 'zustand/vanilla'

export const sharedStore = createStore((set) => ({
  user: null,
  login: (userData) => set({ user: userData }),
  logout: () => set({ user: null }),
}))

// react-app.js - React 应用
import { create } from 'zustand'
import { sharedStore } from './store'

const useSharedStore = create(() => sharedStore.getState())

function UserProfile() {
  const user = useSharedStore((state) => state.user)
  return <div>{user ? `欢迎, ${user.name}` : '请登录'}</div>
}

// vanilla-app.js - 非 React 应用
import { sharedStore } from './store'

// 登录按钮点击事件
document.getElementById('loginBtn').addEventListener('click', () => {
  sharedStore.getState().login({ name: '张三', id: 1 })
})

// 监听用户状态变化
sharedStore.subscribe(
  (state) => state.user,
  (user) => {
    if (user) {
      document.getElementById('userInfo').textContent = `欢迎, ${user.name}`
    } else {
      document.getElementById('userInfo').textContent = '请登录'
    }
  }
)

高级用法

创建可复用的 Store 工厂

javascript
import { createStore } from 'zustand/vanilla'

// Store 工厂函数
function createCounterStore(initialCount = 0) {
  return createStore((set, get) => ({
    count: initialCount,
    increment: () => set((state) => ({ count: state.count + 1 })),
    decrement: () => set((state) => ({ count: state.count - 1 })),
    reset: () => set({ count: initialCount }),
    getDouble: () => get().count * 2,
  }))
}

// 创建多个独立的 Store 实例
const counter1 = createCounterStore(0)
const counter2 = createCounterStore(10)

counter1.getState().increment() // counter1.count = 1
counter2.getState().increment() // counter2.count = 11

异步 Actions

javascript
import { createStore } from 'zustand/vanilla'

const userStore = createStore((set) => ({
  users: [],
  isLoading: false,
  error: null,
  fetchUsers: async () => {
    set({ isLoading: true, error: null })
    try {
      const response = await fetch('/api/users')
      const users = await response.json()
      set({ users, isLoading: false })
    } catch (error) {
      set({ error: error.message, isLoading: false })
    }
  },
}))

// 调用异步 Action
userStore.getState().fetchUsers()

// 监听加载状态
userStore.subscribe(
  (state) => state.isLoading,
  (isLoading) => {
    if (isLoading) {
      document.getElementById('loading').style.display = 'block'
    } else {
      document.getElementById('loading').style.display = 'none'
    }
  }
)

状态切片模式

javascript
import { createStore } from 'zustand/vanilla'

// 用户切片
const createUserSlice = (set) => ({
  user: null,
  login: (userData) => set({ user: userData }),
  logout: () => set({ user: null }),
})

// 计数器切片
const createCounterSlice = (set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  incrementIfLoggedIn: () => {
    if (get().user) {
      set((state) => ({ count: state.count + 1 }))
    }
  },
})

// 组合切片创建 Store
const store = createStore((set, get) => ({
  ...createUserSlice(set),
  ...createCounterSlice(set, get),
}))

最佳实践

1. 模块化 Store

将 Store 按功能模块化,便于维护和测试:

javascript
// stores/user.js
import { createStore } from 'zustand/vanilla'

export const userStore = createStore((set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
}))

// stores/counter.js
import { createStore } from 'zustand/vanilla'

export const counterStore = createStore((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

2. 合理使用订阅

只订阅必要的状态变化,避免不必要的重新渲染或计算:

javascript
// 好的做法:只订阅需要的状态
store.subscribe(
  (state) => state.count,
  (count) => {
    // 只在 count 变化时执行
    updateCountDisplay(count)
  }
)

// 避免:订阅整个状态
store.subscribe((state) => {
  // 每次状态变化都会执行,即使与 count 无关
  updateCountDisplay(state.count)
})

3. 及时取消订阅

在组件卸载或不再需要时取消订阅,避免内存泄漏:

javascript
let unsubscribe

function initApp() {
  unsubscribe = store.subscribe((state) => {
    // 处理状态变化
  })
}

function destroyApp() {
  if (unsubscribe) {
    unsubscribe()
  }
}

// 页面加载时初始化
window.addEventListener('load', initApp)

// 页面卸载时清理
window.addEventListener('unload', destroyApp)

4. 使用 TypeScript 增强类型安全

typescript
import { createStore } from 'zustand/vanilla'

interface CounterState {
  count: number
  increment: () => void
  decrement: () => void
}

const counterStore = createStore<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}))

5. 避免过度使用全局状态

虽然 Zustand 的 vanilla 模式很方便,但仍应避免将所有状态都放在全局 Store 中。只将需要在多个组件或模块间共享的状态放入 Store。

与其他状态管理方案比较

特性 Zustand Vanilla Redux (纯 JS) MobX
学习曲线 低 高 中
样板代码 极少 大量 中
中间件支持 丰富 丰富 有限
调试工具 支持 Redux DevTools 支持 支持
类型安全 良好 良好 良好
性能 优秀 良好 良好

常见问题与解决方案

1. 状态更新后订阅函数不触发

问题:调用了状态更新方法,但订阅函数没有触发

解决方案:

  • 确保调用的是 Store 实例的方法,而不是普通函数
  • 检查订阅是否在更新之前创建
  • 确保没有提前取消订阅
javascript
// 错误示例:调用普通函数
const increment = counterStore.getState().increment
increment() // 可能不会触发订阅

// 正确示例:直接调用 Store 方法
counterStore.getState().increment() // 会触发订阅

2. 在 React 中使用时状态不同步

问题:React 组件中的状态与 vanilla Store 中的状态不同步

解决方案:确保使用正确的方式将 vanilla Store 转换为 React Hook:

javascript
// 错误示例:每次渲染都创建新的 Hook
function Component() {
  const useStore = create(() => vanillaStore.getState())
  const state = useStore()
  // ...
}

// 正确示例:在组件外部创建 Hook
const useStore = create(() => vanillaStore.getState())

function Component() {
  const state = useStore()
  // ...
}

3. 中间件不工作

问题:添加了中间件(如 persist),但功能不生效

解决方案:

  • 确保使用的是 zustand/vanilla 而不是 zustand
  • 检查中间件的导入和使用方式
  • 确保中间件的顺序正确
javascript
// 错误示例:使用了 React 版本的中间件
import { persist } from 'zustand/middleware'
import { createStore } from 'zustand' // 应该使用 zustand/vanilla

// 正确示例
import { persist } from 'zustand/middleware'
import { createStore } from 'zustand/vanilla'

总结

Zustand 的 vanilla 模式为非 React 环境提供了强大的状态管理能力:

  1. 简单易用:API 与 React 版本一致,学习成本低
  2. 灵活多样:支持所有中间件和高级功能
  3. 跨框架共享:可以在 React 和非 React 之间共享状态
  4. 高性能:订阅机制确保只在需要时更新

通过遵循本指南中的最佳实践,你可以在各种环境中充分利用 Zustand 的强大功能,构建可靠、可维护的状态管理解决方案。

在接下来的章节中,我们将学习 切片模式 (Slice Pattern),了解如何更好地组织复杂的 Store 结构。