非 React 组件中使用 Store
Zustand 虽然主要设计用于 React 应用,但它也提供了 "vanilla"(纯 JavaScript)模式,允许你在非 React 环境中使用 Zustand Store。这对于混合应用、旧项目迁移或需要在多个框架间共享状态的场景非常有用。
Vanilla 模式的基本概念
在 vanilla 模式下,Zustand 提供了一个独立的 Store 实例,不依赖于 React 的上下文或钩子。你可以:
- 创建独立的 Store 实例
- 直接访问和更新状态
- 订阅状态变化
- 使用所有 Zustand 中间件
创建 Vanilla Store
安装
首先确保已经安装了 Zustand:
npm install zustand基本创建方式
使用 createStore 函数创建 vanilla Store:
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,
}))访问和更新状态
获取当前状态
// 获取当前状态
const currentState = counterStore.getState()
console.log(currentState.count) // 输出: 0更新状态
// 更新状态
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 方法更新状态:
// 直接使用 set 方法
counterStore.setState((state) => ({ count: state.count + 1 }))
console.log(counterStore.getState().count) // 输出: 1订阅状态变化
基本订阅
使用 subscribe 方法监听状态变化:
// 订阅所有状态变化
const unsubscribe = counterStore.subscribe((newState, oldState) => {
console.log('状态变化:', { oldState, newState })
})
// 触发状态变化
counterStore.getState().increment()
// 输出: 状态变化: { oldState: { count: 0 }, newState: { count: 1 } }选择性订阅
你可以通过 Selector 函数只订阅感兴趣的状态部分:
// 只订阅 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 中间件支持浅比较:
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 函数:
const unsubscribe = counterStore.subscribe((newState) => {
console.log('状态变化:', newState)
})
// 取消订阅
unsubscribe()
// 后续的状态变化将不再触发回调
counterStore.getState().increment() // 不会输出任何内容取消所有订阅
使用 destroy 方法取消所有订阅并清理 Store:
// 取消所有订阅并清理 Store
counterStore.destroy()与中间件一起使用
Vanilla Store 支持所有 Zustand 中间件,如 persist、immer 和 devtools。
Persist 中间件
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 中间件
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 中间件
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
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 之间共享状态
// 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 工厂
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
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'
}
}
)状态切片模式
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 按功能模块化,便于维护和测试:
// 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. 合理使用订阅
只订阅必要的状态变化,避免不必要的重新渲染或计算:
// 好的做法:只订阅需要的状态
store.subscribe(
(state) => state.count,
(count) => {
// 只在 count 变化时执行
updateCountDisplay(count)
}
)
// 避免:订阅整个状态
store.subscribe((state) => {
// 每次状态变化都会执行,即使与 count 无关
updateCountDisplay(state.count)
})3. 及时取消订阅
在组件卸载或不再需要时取消订阅,避免内存泄漏:
let unsubscribe
function initApp() {
unsubscribe = store.subscribe((state) => {
// 处理状态变化
})
}
function destroyApp() {
if (unsubscribe) {
unsubscribe()
}
}
// 页面加载时初始化
window.addEventListener('load', initApp)
// 页面卸载时清理
window.addEventListener('unload', destroyApp)4. 使用 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 实例的方法,而不是普通函数
- 检查订阅是否在更新之前创建
- 确保没有提前取消订阅
// 错误示例:调用普通函数
const increment = counterStore.getState().increment
increment() // 可能不会触发订阅
// 正确示例:直接调用 Store 方法
counterStore.getState().increment() // 会触发订阅2. 在 React 中使用时状态不同步
问题:React 组件中的状态与 vanilla Store 中的状态不同步
解决方案:确保使用正确的方式将 vanilla Store 转换为 React Hook:
// 错误示例:每次渲染都创建新的 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
- 检查中间件的导入和使用方式
- 确保中间件的顺序正确
// 错误示例:使用了 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 环境提供了强大的状态管理能力:
- 简单易用:API 与 React 版本一致,学习成本低
- 灵活多样:支持所有中间件和高级功能
- 跨框架共享:可以在 React 和非 React 之间共享状态
- 高性能:订阅机制确保只在需要时更新
通过遵循本指南中的最佳实践,你可以在各种环境中充分利用 Zustand 的强大功能,构建可靠、可维护的状态管理解决方案。
在接下来的章节中,我们将学习 切片模式 (Slice Pattern),了解如何更好地组织复杂的 Store 结构。