Zustand 状态管理模式
来自 14 个模块共用 Zustand 的生产级应用的实战经验。
适用场景
- React + Zustand 项目的状态管理设计
- 多模块 Store 拆分与复用
- 持久化 + 应用重启后恢复
- Electron 主进程 ↔ Store 联动
- Store 测试
1. Store 设计规范
一个模块一个 Store
// ✅ 每个功能模块独立 Store
src/modules/video-compressor/store/index.ts → useVideoCompressorStore
src/modules/video-upscaler/store/index.ts → useVideoUpscalerStore
// ❌ 不要把所有状态塞进一个全局 Store
src/store/globalStore.ts → useGlobalStore // 千万别这样
Store 命名
// Hook 导出用 use 前缀 + 模块名 + Store
export const useVideoCompressorStore = create<VideoCompressorStore>()(...)
// 文件名:index.ts 或 {moduleName}Store.ts
Store 接口先行
// ✅ 先定义接口,再实现
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 命名
// 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
// 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
interface MyModuleStore extends ProcessingSliceState {
inputFiles: string[];
// ...
}
const useMyModuleStore = create<MyModuleStore>()((set) => ({
...createProcessingSlice(set), // 展开混入
inputFiles: [],
// ...
}));
泛型 Slice
// 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. 持久化
基本持久化
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 等运行时状态
}),
}
)
);
关键原则
// ✅ 持久化:用户配置、偏好设置
partialize: (state) => ({
outputDir: state.outputDir,
quality: state.quality,
encoder: state.encoder,
})
// ❌ 不持久化:运行时状态
// isProcessing, progress, logs, error — 这些重启后应该重置
Electron 存储
Electron 中 localStorage 可用(渲染进程),但如果需要主进程访问,用 electron-store:
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 视频生成)提交后,应用重启需要恢复轮询:
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 更新
// 渲染进程:监听主进程事件更新 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 调用
// 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() 防闭包
// ❌ 闭包陷阱:回调函数中的 state 是旧的
window.electronAPI.on('update', () => {
const { tasks } = store; // 闭包捕获的旧值!
});
// ✅ 每次用 getState() 获取最新
window.electronAPI.on('update', () => {
const { tasks } = useMyStore.getState(); // 始终最新
});
6. Store 测试
测试模板
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
// 测试持久化时 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. 常见陷阱
闭包过期
// ❌ 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);
}, []);
过度订阅
// ❌ 订阅整个 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 }))
);
循环更新
// ❌ 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