| name | zustand-patterns |
| description | Zustand 状态管理实战模式。涵盖 Store 设计规范、Slice 工厂复用、persist 持久化、可恢复任务持久化、Electron IPC 联动、Store 测试和常见陷阱。适用于 React + Zustand 项目。 |
Zustand 状态管理模式
来自 14 个模块共用 Zustand 的生产级应用的实战经验。
适用场景
- React + Zustand 项目的状态管理设计
- 多模块 Store 拆分与复用
- 持久化 + 应用重启后恢复
- Electron 主进程 ↔ Store 联动
- Store 测试
1. Store 设计规范
一个模块一个 Store
src/modules/video-compressor/store/index.ts → useVideoCompressorStore
src/modules/video-upscaler/store/index.ts → useVideoUpscalerStore
src/store/globalStore.ts → useGlobalStore
Store 命名
export const useVideoCompressorStore = create<VideoCompressorStore>()(...)
Store 接口先行
interface VideoCompressorStore {
inputFiles: string[];
outputDir: string;
targetSizeMB: number;
logs: LogEntry[];
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 命名
setInputFiles: (files) => set({ inputFiles: files }),
setTargetSizeMB: (size) => set({ targetSizeMB: size }),
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)
})),
clearInputFiles: () => set({ inputFiles: [] }),
clearLogs: () => set({ logs: [] }),
reset: () => set({ inputFiles: [], outputDir: '', targetSizeMB: 50, : [] }),
2. Slice 工厂(跨 Store 复用)
多个 Store 有相同的状态片段时,用 Slice 工厂提取:
定义 Slice
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) => ({ progress } ),
: ({ : , : defaultProgress } ),
};
}
使用 Slice
interface MyModuleStore extends ProcessingSliceState {
inputFiles: string[];
}
const useMyModuleStore = create<MyModuleStore>()((set) => ({
...createProcessingSlice(set),
inputFiles: [],
}));
泛型 Slice
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',
version: 1,
partialize: (state) => ({
outputDir: state.outputDir,
targetSizeMB: state.targetSizeMB,
}),
}
)
);
关键原则
partialize: (state) => ({
outputDir: state.outputDir,
quality: state.quality,
encoder: state.encoder,
})
Electron 存储
Electron 中 localStorage 可用(渲染进程),但如果需要主进程访问,用 electron-store:
import { persist, createJSONStorage } from 'zustand/middleware';
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) => {
const tasks = (state as any)[taskField] || [];
if (Array.isArray(tasks) && tasks.some(isTaskPending)) {
(state as any).needsPollingRecovery = true;
}
},
};
}
const store = create<MyState>()(
(
({ }),
({
: ,
: ,
: task. === && !!task.,
: [],
})
)
);
( {
(store.) {
store.();
store.();
}
}, []);
适用 vs 不适用
✅ 适用:远程 API 任务(视频超分、AI 生成)— 服务端继续处理
❌ 不适用:本地进程任务(FFmpeg 压缩)— 进程随应用关闭而终止
5. Electron IPC ↔ 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, );
}),
];
listeners.( ());
}, []);
Store Action → 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() 防闭包
window.electronAPI.on('update', () => {
const { tasks } = store;
});
window.electronAPI.on('update', () => {
const { tasks } = useMyStore.getState();
});
6. Store 测试
测试模板
import { act } from 'react';
import { useVideoCompressorStore } from '../store';
describe('VideoCompressorStore', () => {
beforeEach(() => {
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();
useVideoCompressorStore.().();
});
state = useVideoCompressorStore.();
(state.).([]);
(state.).();
});
});
测试 Persist
beforeEach(() => {
localStorage.clear();
});
it('should persist and rehydrate config', () => {
act(() => {
useSettingsStore.getState().setTargetSizeMB(100);
});
const persisted = JSON.parse(localStorage.getItem('settings-storage') || '{}');
expect(persisted.state.targetSizeMB).toBe(100);
});
7. 常见陷阱
闭包过期
const { tasks } = useMyStore();
useEffect(() => {
const interval = setInterval(() => {
console.log(tasks);
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const interval = setInterval(() => {
console.log(useMyStore.getState().tasks);
}, 1000);
return () => clearInterval(interval);
}, []);
过度订阅
const store = useMyStore();
const isProcessing = useMyStore((s) => s.isProcessing);
const progress = useMyStore((s) => s.progress);
import { useShallow } from 'zustand/react/shallow';
const { files, dir } = useMyStore(
useShallow((s) => ({ files: s.inputFiles, dir: s.outputDir }))
);
循环更新
useEffect(() => {
useMyStore.getState().setProgress(calculateProgress());
}, [someValue]);
useMyStore.subscribe(
(state) => state.tasks,
(tasks) => { },
{ equalityFn: shallow }
);
8. Checklist
新建 Store
使用 Store
测试