问题背景

在使用 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'
  • 但组件中获取到的 deviceIdaccessCode 始终为空字符串
  • 子 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,原因如下:

  1. 简单直接:避免了复杂的订阅逻辑
  2. 性能优化:减少了不必要的状态代理
  3. 代码清晰:组件直接使用需要的子 store,意图明确
  4. 维护性好:减少了中间层,降低了复杂度

修复后的代码

// 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'
  
  // ... 其他组件逻辑
};

经验总结

  1. 避免静态状态组合:不要使用 ...store.getState() 来组合状态
  2. 选择合适的状态管理策略
    • 简单场景:直接使用子 store
    • 复杂场景:使用订阅机制
  3. 保持状态响应性:确保状态变化能够正确传播
  4. 考虑性能影响:减少不必要的状态代理和订阅

相关技术要点

  • Zustand 订阅机制store.subscribe(callback)
  • 状态响应性:确保状态变化能够触发组件重新渲染
  • Store 组合模式:合理设计 store 的层次结构
  • 性能优化:避免不必要的状态同步和组件重渲染

通过这个案例,我们深入理解了 Zustand 中状态组合的陷阱,并学会了如何正确设计复杂的状态管理架构。