问题背景
在使用 Zustand 构建复杂状态管理时,经常会遇到需要组合多个子 store 的场景。本文通过一个实际案例,分析了在组合 store 时遇到的状态同步问题,并提供了相应的解决方案。
问题描述
原始代码结构
const useDeviceStore = create((set, get) => ({
// 问题:这种方式只获取初始状态,不会响应子store的变化
...useDeviceIdentityStore.getState(),
...useNetworkStatusStore.getState(),
...useOperationStatusStore.getState(),
...useDeviceInfoStore.getState(),
_initialized: false,
init: async () => {
// 初始化逻辑
await useDeviceIdentityStore.getState().init();
set({ _initialized: true });
}
}));
// src/pages/mainTab/index.jsx - 组件使用
const MainTab = () => {
const deviceId = useDeviceStore((state) => state.deviceId); // 始终为空
const accessCode = useDeviceStore((state) => state.accessCode); // 始终为空
// ...
};
问题现象
- 设备初始化成功(控制台显示
deviceId: 'E00E40', accessCode: 'CJRA88') - 但组件中获取到的
deviceId和accessCode始终为空字符串 - 子 store 状态正常,主 store 状态不同步
问题根源
状态组合方式错误:使用 ...useDeviceIdentityStore.getState() 只会在 store 创建时获取一次初始状态,不会建立响应式连接。当子 store 状态更新时,主 store 不会自动同步。
解决方案对比
方案1:订阅机制(推荐)
优点:
- 保持原有 API 结构
- 自动状态同步
- 类型安全
- 易于维护
缺点:
- 代码稍复杂
- 需要额外的订阅逻辑
// src/store/device/index.js - 修复后的主 store
const useDeviceStore = create((set, get) => ({
_initialized: false,
init: async () => {
await useDeviceIdentityStore.getState().init();
set({ _initialized: true });
}
}));
// 订阅子store的变化并同步到主store
useDeviceIdentityStore.subscribe((state) => {
useDeviceStore.setState({
deviceId: state.deviceId,
accessCode: state.accessCode,
});
});
useNetworkStatusStore.subscribe((state) => {
useDeviceStore.setState({
networkConnectedStatus: state.networkConnectedStatus,
});
});
方案2:直接使用子 store
优点:
- 实现简单
- 性能更好(减少一层代理)
- 状态直接响应
缺点:
- 破坏原有 API 结构
- 组件需要知道具体的子 store
- 可能导致组件耦合度增加
// src/pages/mainTab/index.jsx - 直接使用子 store
import useDeviceIdentityStore from '@/store/device/deviceIdentity';
import useNetworkStatusStore from '@/store/device/networkStatus';
import useDeviceInfoStore from '@/store/device/deviceInfo';
const MainTab = () => {
const deviceId = useDeviceIdentityStore((state) => state.deviceId);
const accessCode = useDeviceIdentityStore((state) => state.accessCode);
const networkConnectedStatus = useNetworkStatusStore((state) => state.networkConnectedStatus);
const linkStatus = useDeviceInfoStore((state) => state.linkStatus);
// ...
};
最终选择
根据项目实际情况,我们选择了方案2,原因如下:
- 简单直接:避免了复杂的订阅逻辑
- 性能优化:减少了不必要的状态代理
- 代码清晰:组件直接使用需要的子 store,意图明确
- 维护性好:减少了中间层,降低了复杂度
修复后的代码
// src/pages/mainTab/index.jsx
import useDeviceIdentityStore from '@/store/device/deviceIdentity';
import useNetworkStatusStore from '@/store/device/networkStatus';
import useDeviceInfoStore from '@/store/device/deviceInfo';
const MainTab = () => {
// 直接从对应的子 store 获取状态
const deviceId = useDeviceIdentityStore((state) => state.deviceId);
const accessCode = useDeviceIdentityStore((state) => state.accessCode);
const networkConnectedStatus = useNetworkStatusStore((state) => state.networkConnectedStatus);
const linkStatus = useDeviceInfoStore((state) => state.linkStatus);
const updateAccessCode = useDeviceIdentityStore((state) => state.updateAccessCode);
// 现在 deviceId 和 accessCode 能正确获取到值
console.log('deviceId:', deviceId); // 'E00E40'
console.log('accessCode:', accessCode); // 'CJRA88'
// ... 其他组件逻辑
};
经验总结
- 避免静态状态组合:不要使用
...store.getState()来组合状态 - 选择合适的状态管理策略:
- 简单场景:直接使用子 store
- 复杂场景:使用订阅机制
- 保持状态响应性:确保状态变化能够正确传播
- 考虑性能影响:减少不必要的状态代理和订阅
相关技术要点
- Zustand 订阅机制:
store.subscribe(callback) - 状态响应性:确保状态变化能够触发组件重新渲染
- Store 组合模式:合理设计 store 的层次结构
- 性能优化:避免不必要的状态同步和组件重渲染
通过这个案例,我们深入理解了 Zustand 中状态组合的陷阱,并学会了如何正确设计复杂的状态管理架构。