# 非 React 组件中使用 Store

URL: https://caijiao.org/zustand/guide/vanilla-js-usage
Source: docs/zustand/guide/vanilla-js-usage.md
Description: 了解如何在非 React 环境中使用 Zustand 的 vanilla 模式，包括创建独立 Store 实例、直接访问和更新状态、订阅状态变化、使用中间件以及 TypeScript 支持等内容。

[Zustand](../) 虽然主要设计用于 React 应用，但它也提供了 "vanilla"（纯 [JavaScript](/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)](slice-pattern.md)，了解如何更好地组织复杂的 Store 结构。
