Back to skills
SkillHub ClubShip Full StackFull StackFrontend

zustand-patterns

Zustand 状态管理实战模式。涵盖 Store 设计规范、Slice 工厂复用、persist 持久化、可恢复任务持久化、Electron IPC 联动、Store 测试和常见陷阱。适用于 React + Zustand 项目。

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,112
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B70.4

Install command

npx @skill-hub/cli install openclaw-skills-zustand-patterns

Repository

openclaw/skills

Skill path: skills/bingfoon/zustand-patterns

Zustand 状态管理实战模式。涵盖 Store 设计规范、Slice 工厂复用、persist 持久化、可恢复任务持久化、Electron IPC 联动、Store 测试和常见陷阱。适用于 React + Zustand 项目。

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Frontend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install zustand-patterns into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding zustand-patterns to shared team environments
  • Use zustand-patterns for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: zustand-patterns
version: 1.0.0
description: Zustand 状态管理实战模式。涵盖 Store 设计规范、Slice 工厂复用、persist 持久化、可恢复任务持久化、Electron IPC 联动、Store 测试和常见陷阱。适用于 React + Zustand 项目。
---

# Zustand 状态管理模式

来自 14 个模块共用 Zustand 的生产级应用的实战经验。

## 适用场景

- React + Zustand 项目的状态管理设计
- 多模块 Store 拆分与复用
- 持久化 + 应用重启后恢复
- Electron 主进程 ↔ Store 联动
- Store 测试

---

## 1. Store 设计规范

### 一个模块一个 Store

```typescript
// ✅ 每个功能模块独立 Store
src/modules/video-compressor/store/index.ts   → useVideoCompressorStore
src/modules/video-upscaler/store/index.ts     → useVideoUpscalerStore

// ❌ 不要把所有状态塞进一个全局 Store
src/store/globalStore.ts → useGlobalStore  // 千万别这样
```

### Store 命名

```typescript
// Hook 导出用 use 前缀 + 模块名 + Store
export const useVideoCompressorStore = create<VideoCompressorStore>()(...)

// 文件名:index.ts 或 {moduleName}Store.ts
```

### Store 接口先行

```typescript
// ✅ 先定义接口,再实现
interface VideoCompressorStore {
  // — 状态 —
  inputFiles: string[];
  outputDir: string;
  targetSizeMB: number;
  logs: LogEntry[];

  // — Actions —
  setInputFiles: (files: string[]) => void;
  addInputFiles: (files: string[]) => void;
  removeInputFile: (path: string) => void;
  reset: () => void;
}

export const useVideoCompressorStore = create<VideoCompressorStore>()(
  persist(
    (set) => ({
      // 实现...
    }),
    { name: 'video-compressor' }
  )
);
```

### Action 命名

```typescript
// set 前缀:简单赋值
setInputFiles: (files) => set({ inputFiles: files }),
setTargetSizeMB: (size) => set({ targetSizeMB: size }),

// add/remove 前缀:集合操作
addInputFiles: (files) => set((state) => ({
  inputFiles: [...state.inputFiles, ...files.filter(f => !state.inputFiles.includes(f))]
})),
removeInputFile: (path) => set((state) => ({
  inputFiles: state.inputFiles.filter(p => p !== path)
})),

// clear 前缀:清空
clearInputFiles: () => set({ inputFiles: [] }),
clearLogs: () => set({ logs: [] }),

// reset:恢复初始状态
reset: () => set({ inputFiles: [], outputDir: '', targetSizeMB: 50, logs: [] }),
```

---

## 2. Slice 工厂(跨 Store 复用)

多个 Store 有相同的状态片段时,用 Slice 工厂提取:

### 定义 Slice

```typescript
// store/slices/createProcessingSlice.ts

export interface ProcessingSliceState<TProgress = number> {
  isProcessing: boolean;
  progress: TProgress;
  setIsProcessing: (isProcessing: boolean) => void;
  setProgress: (progress: TProgress) => void;
  resetProcessing: () => void;
}

export function createProcessingSlice<TProgress = number>(
  set: SetState<ProcessingSliceState<TProgress>>,
  defaultProgress: TProgress = 0 as TProgress,
): ProcessingSliceState<TProgress> {
  return {
    isProcessing: false,
    progress: defaultProgress,
    setIsProcessing: (isProcessing) => set({ isProcessing } as any),
    setProgress: (progress) => set({ progress } as any),
    resetProcessing: () => set({ isProcessing: false, progress: defaultProgress } as any),
  };
}
```

### 使用 Slice

```typescript
interface MyModuleStore extends ProcessingSliceState {
  inputFiles: string[];
  // ...
}

const useMyModuleStore = create<MyModuleStore>()((set) => ({
  ...createProcessingSlice(set),  // 展开混入
  inputFiles: [],
  // ...
}));
```

### 泛型 Slice

```typescript
// progress 不一定是 number,可以是复杂对象
interface SceneAnalyzerProgress {
  phase: 'splitting' | 'analyzing' | 'done';
  current: number;
  total: number;
}

interface SceneAnalyzerStore extends ProcessingSliceState<SceneAnalyzerProgress | null> {
  // ...
}

const store = create<SceneAnalyzerStore>()((set) => ({
  ...createProcessingSlice<SceneAnalyzerProgress | null>(set, null),
  // ...
}));
```

---

## 3. 持久化

### 基本持久化

```typescript
import { persist } from 'zustand/middleware';

const useSettingsStore = create<SettingsStore>()(
  persist(
    (set) => ({ /* ... */ }),
    {
      name: 'settings-storage',           // localStorage key
      version: 1,                          // 迁移版本
      partialize: (state) => ({            // 只持久化部分字段
        outputDir: state.outputDir,
        targetSizeMB: state.targetSizeMB,
        // ❌ 不持久化 isProcessing、logs 等运行时状态
      }),
    }
  )
);
```

### 关键原则

```typescript
// ✅ 持久化:用户配置、偏好设置
partialize: (state) => ({
  outputDir: state.outputDir,
  quality: state.quality,
  encoder: state.encoder,
})

// ❌ 不持久化:运行时状态
// isProcessing, progress, logs, error — 这些重启后应该重置
```

### Electron 存储

Electron 中 `localStorage` 可用(渲染进程),但如果需要主进程访问,用 `electron-store`:

```typescript
import { persist, createJSONStorage } from 'zustand/middleware';

// 自定义 storage adapter 走 IPC
const electronStorage = createJSONStorage(() => ({
  getItem: (name) => ipcRenderer.invoke('store:get', name),
  setItem: (name, value) => ipcRenderer.invoke('store:set', name, value),
  removeItem: (name) => ipcRenderer.invoke('store:remove', name),
}));
```

---

## 4. 可恢复任务持久化(高级)

远程异步任务(如 AI 视频生成)提交后,应用重启需要恢复轮询:

```typescript
interface RecoverableTaskState {
  needsPollingRecovery: boolean;
  clearPollingRecoveryFlag: () => void;
}

function createRecoverablePersistConfig<T>({
  name,
  taskField,
  isTaskPending,
  additionalFields = [],
}: {
  name: string;
  taskField: keyof T;
  isTaskPending: (task: any) => boolean;
  additionalFields?: (keyof T)[];
}) {
  return {
    name,
    partialize: (state: T) => {
      const result: any = { [taskField]: state[taskField] };
      for (const field of additionalFields) {
        result[field] = state[field];
      }
      return result;
    },
    onRehydrate: (state: T) => {
      // 检查是否有需要恢复的 pending 任务
      const tasks = (state as any)[taskField] || [];
      if (Array.isArray(tasks) && tasks.some(isTaskPending)) {
        (state as any).needsPollingRecovery = true;
      }
    },
  };
}

// 使用
const store = create<MyState>()(
  persist(
    (set, get) => ({ /* ... */ }),
    createRecoverablePersistConfig({
      name: 'video-upscaler',
      taskField: 'tasks',
      isTaskPending: (task) => task.status === 'processing' && !!task.remoteId,
      additionalFields: ['config'],
    })
  )
);

// 组件中恢复轮询
useEffect(() => {
  if (store.needsPollingRecovery) {
    store.clearPollingRecoveryFlag();
    store.recoverPolling();
  }
}, []);
```

### 适用 vs 不适用

```
✅ 适用:远程 API 任务(视频超分、AI 生成)— 服务端继续处理
❌ 不适用:本地进程任务(FFmpeg 压缩)— 进程随应用关闭而终止
```

---

## 5. Electron IPC ↔ Store 联动

### 主进程事件 → Store 更新

```typescript
// 渲染进程:监听主进程事件更新 Store
useEffect(() => {
  const listeners = [
    window.electronAPI.on('module:progress', (progress: number) => {
      useMyStore.getState().setProgress(progress);
    }),
    window.electronAPI.on('module:complete', () => {
      useMyStore.getState().setIsProcessing(false);
      useMyStore.getState().setProgress(100);
    }),
    window.electronAPI.on('module:error', (error: string) => {
      useMyStore.getState().setIsProcessing(false);
      useMyStore.getState().setError(error);
    }),
    window.electronAPI.on('module:log', (msg: string, type: string) => {
      useMyStore.getState().addLog(msg, type);
    }),
  ];

  return () => listeners.forEach(cleanup => cleanup());
}, []);
```

### Store Action → IPC 调用

```typescript
// Store 中发起 IPC 调用
startProcessing: async () => {
  const { inputFiles, outputDir, targetSizeMB } = get();
  set({ isProcessing: true, progress: 0 });

  try {
    await window.electronAPI.invoke('module:start', {
      files: inputFiles,
      outputDir,
      targetSizeMB,
    });
  } catch (error) {
    set({ isProcessing: false, error: getErrorMessage(error) });
  }
},

stopProcessing: () => {
  window.electronAPI.invoke('module:stop');
},
```

### 关键:`getState()` 防闭包

```typescript
// ❌ 闭包陷阱:回调函数中的 state 是旧的
window.electronAPI.on('update', () => {
  const { tasks } = store; // 闭包捕获的旧值!
});

// ✅ 每次用 getState() 获取最新
window.electronAPI.on('update', () => {
  const { tasks } = useMyStore.getState(); // 始终最新
});
```

---

## 6. Store 测试

### 测试模板

```typescript
import { act } from 'react';
import { useVideoCompressorStore } from '../store';

describe('VideoCompressorStore', () => {
  beforeEach(() => {
    // 每个测试前重置 Store
    act(() => {
      useVideoCompressorStore.getState().reset();
    });
  });

  it('should add input files without duplicates', () => {
    act(() => {
      useVideoCompressorStore.getState().addInputFiles(['/a.mp4', '/b.mp4']);
      useVideoCompressorStore.getState().addInputFiles(['/b.mp4', '/c.mp4']);
    });

    const { inputFiles } = useVideoCompressorStore.getState();
    expect(inputFiles).toEqual(['/a.mp4', '/b.mp4', '/c.mp4']);
  });

  it('should reset to initial state', () => {
    act(() => {
      useVideoCompressorStore.getState().setInputFiles(['/a.mp4']);
      useVideoCompressorStore.getState().setIsProcessing(true);
      useVideoCompressorStore.getState().reset();
    });

    const state = useVideoCompressorStore.getState();
    expect(state.inputFiles).toEqual([]);
    expect(state.isProcessing).toBe(false);
  });
});
```

### 测试 Persist

```typescript
// 测试持久化时 mock localStorage
beforeEach(() => {
  localStorage.clear();
});

it('should persist and rehydrate config', () => {
  act(() => {
    useSettingsStore.getState().setTargetSizeMB(100);
  });

  // 模拟刷新:重新创建 store
  // zustand persist 会从 localStorage 读取
  const persisted = JSON.parse(localStorage.getItem('settings-storage') || '{}');
  expect(persisted.state.targetSizeMB).toBe(100);
});
```

---

## 7. 常见陷阱

### 闭包过期

```typescript
// ❌ useEffect 中直接用解构的值
const { tasks } = useMyStore();
useEffect(() => {
  const interval = setInterval(() => {
    console.log(tasks); // 永远是初始值!
  }, 1000);
  return () => clearInterval(interval);
}, []); // deps 为空

// ✅ 用 getState()
useEffect(() => {
  const interval = setInterval(() => {
    console.log(useMyStore.getState().tasks); // 最新值
  }, 1000);
  return () => clearInterval(interval);
}, []);
```

### 过度订阅

```typescript
// ❌ 订阅整个 Store(任何字段变化都重渲染)
const store = useMyStore();

// ✅ 只订阅需要的字段
const isProcessing = useMyStore((s) => s.isProcessing);
const progress = useMyStore((s) => s.progress);

// ✅ 多字段用 shallow 比较
import { useShallow } from 'zustand/react/shallow';
const { files, dir } = useMyStore(
  useShallow((s) => ({ files: s.inputFiles, dir: s.outputDir }))
);
```

### 循环更新

```typescript
// ❌ useEffect 中 set 触发重渲染 → 再触发 useEffect
useEffect(() => {
  useMyStore.getState().setProgress(calculateProgress());
}, [someValue]); // someValue 也来自同一个 Store → 死循环

// ✅ 用 subscribe 或在 action 内部处理
useMyStore.subscribe(
  (state) => state.tasks,
  (tasks) => { /* 根据 tasks 更新 progress */ },
  { equalityFn: shallow }
);
```

---

## 8. Checklist

### 新建 Store
- [ ] 接口先行(先写 interface 再实现)
- [ ] 一模块一 Store,用 `use` 前缀命名
- [ ] 复用 Slice 工厂(ProcessingSlice 等)
- [ ] `partialize` 只持久化配置,不持久化运行时状态
- [ ] Action 命名:set / add / remove / clear / reset

### 使用 Store
- [ ] 组件中用选择器订阅,不订阅整个 Store
- [ ] 回调/定时器中用 `getState()` 防闭包
- [ ] IPC 事件监听在 useEffect 中注册和清理

### 测试
- [ ] `beforeEach` 中 `reset()` Store
- [ ] 测试 action 用 `act()` 包裹
- [ ] 持久化测试 mock `localStorage`


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "bingfoon",
  "slug": "zustand-patterns",
  "displayName": "Zustand Patterns",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1772402346186,
    "commit": "https://github.com/openclaw/skills/commit/3398c72e6827b63cb4307cef72392a58e170d7fc"
  },
  "history": []
}

```

zustand-patterns | SkillHub