康心伴Logo
康心伴WellAlly
移动开发

Zustand vs Redux:React Native 健康应用状态管理对比

5 分钟阅读

Zustand vs Redux:React Native 健康应用状态管理对比

概述

在 React Native 健康应用开发中,选择合适的状态管理方案至关重要。用户需要离线访问健康数据、同步运动记录、管理设备连接等。本文将深入对比 Zustand 和 Redux 在 React Native 健康应用中的实践。

React Native 特定考量

移动健康应用有特殊需求:

  • 离线优先:数据需要在本地持久化
  • 后台同步:与健康应用同步
  • 蓝牙连接:管理可穿戴设备
  • 推送通知:健康提醒
  • 性能敏感:低端设备体验

Zustand 实践

核心状态管理

code
// src/stores/healthStore.ts

import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { AsyncStorage } from 'react-native';
import { immer } from 'zustand/middleware/immer';

// 类型定义
interface HealthMetric {
  id: string;
  timestamp: number;
  type: 'steps' | 'heartRate' | 'sleep' | 'calories';
  value: number;
  synced: boolean;
}

interface DeviceConnection {
  id: string;
  name: string;
  connected: boolean;
  lastSync: number | null;
  batteryLevel: number | null;
}

interface NotificationSettings {
  enabled: boolean;
  reminderTime: string; // HH:mm
  inactiveAlerts: boolean;
  goalAchievements: boolean;
}

interface HealthState {
  // 用户数据
  userId: string | null;
  profile: {
    name: string;
    age: number;
    weight: number;
    height: number;
    goals: {
      dailySteps: number;
      weeklyExercise: number;
    };
  } | null;

  // 健康数据
  metrics: HealthMetric[];

  // 设备连接
  devices: Map<string, DeviceConnection>;

  // UI 状态
  selectedDate: Date;
  isLoading: boolean;
  syncStatus: 'idle' | 'syncing' | 'success' | 'error';
  error: string | null;

  // 通知设置
  notifications: NotificationSettings;

  // Actions
  setUserId: (id: string) => void;
  updateProfile: (profile: Partial<HealthState['profile']>) => void;
  addMetric: (metric: Omit<HealthMetric, 'id' | 'synced'>) => void;
  syncMetrics: () => Promise<void>;
  connectDevice: (device: DeviceConnection) => void;
  disconnectDevice: (deviceId: string) => void;
  updateNotificationSettings: (settings: Partial<NotificationSettings>) => void;
  clearError: () => void;
}

export const useHealthStore = create<HealthState>()(
  devtools(
    persist(
      immer((set, get) => ({
        // 初始状态
        userId: null,
        profile: null,
        metrics: [],
        devices: new Map(),
        selectedDate: new Date(),
        isLoading: false,
        syncStatus: 'idle',
        error: null,
        notifications: {
          enabled: true,
          reminderTime: '09:00',
          inactiveAlerts: true,
          goalAchievements: true
        },

        // Actions
        setUserId: (id) =>
          set((state) => {
            state.userId = id;
          }),

        updateProfile: (profile) =>
          set((state) => {
            if (state.profile) {
              state.profile = { ...state.profile, ...profile };
            }
          }),

        addMetric: (metric) =>
          set((state) => {
            state.metrics.push({
              ...metric,
              id: crypto.randomUUID(),
              synced: false
            });
          }),

        syncMetrics: async () => {
          const { userId, metrics } = get();
          if (!userId) return;

          set((state) => {
            state.syncStatus = 'syncing';
            state.error = null;
          });

          try {
            const unsyncedMetrics = metrics.filter((m) => !m.synced);

            for (const metric of unsyncedMetrics) {
              const response = await fetch('/api/health/metrics', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                  userId,
                  ...metric
                })
              });

              if (response.ok) {
                set((state) => {
                  const m = state.metrics.find((m) => m.id === metric.id);
                  if (m) m.synced = true;
                });
              }
            }

            set((state) => {
              state.syncStatus = 'success';
            });
          } catch (error) {
            set((state) => {
              state.error = error instanceof Error ? error.message : '同步失败';
              state.syncStatus = 'error';
            });
          }
        },

        connectDevice: (device) =>
          set((state) => {
            state.devices.set(device.id, {
              ...device,
              connected: true,
              lastSync: null
            });
          }),

        disconnectDevice: (deviceId) =>
          set((state) => {
            const device = state.devices.get(deviceId);
            if (device) {
              device.connected = false;
            }
          }),

        updateNotificationSettings: (settings) =>
          set((state) => {
            state.notifications = { ...state.notifications, ...settings };
          }),

        clearError: () =>
          set((state) => {
            state.error = null;
          })
      })),
      {
        name: 'health-storage',
        storage: createJSONStorage(() => AsyncStorage),
        partialize: (state) => ({
          userId: state.userId,
          profile: state.profile,
          metrics: state.metrics,
          notifications: state.notifications
        })
      }
    ),
    { name: 'HealthStore' }
  )
);

// 便捷 hooks
export const useProfile = () => useHealthStore((state) => state.profile);
export const useMetrics = () => useHealthStore((state) => state.metrics);
export const useDevices = () => useHealthStore((state) => state.devices);
export const useSyncStatus = () => useHealthStore((state) => state.syncStatus);
Code collapsed

Redux 实践

code
// src/redux/healthSlice.ts

import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';

interface HealthMetric {
  id: string;
  timestamp: number;
  type: 'steps' | 'heartRate' | 'sleep' | 'calories';
  value: number;
  synced: boolean;
}

interface HealthState {
  userId: string | null;
  profile: {
    name: string;
    age: number;
    weight: number;
    height: number;
    goals: {
      dailySteps: number;
      weeklyExercise: number;
    };
  } | null;
  metrics: HealthMetric[];
  selectedDate: string;
  isLoading: boolean;
  syncStatus: 'idle' | 'syncing' | 'success' | 'error';
  error: string | null;
}

const initialState: HealthState = {
  userId: null,
  profile: null,
  metrics: [],
  selectedDate: new Date().toISOString(),
  isLoading: false,
  syncStatus: 'idle',
  error: null
};

// Async actions
export const syncMetrics = createAsyncThunk(
  'health/syncMetrics',
  async (_, { getState }) => {
    const state = getState() as { health: HealthState };
    const { userId, metrics } = state.health;

    if (!userId) throw new Error('用户未登录');

    const unsyncedMetrics = metrics.filter((m) => !m.synced);

    const results = await Promise.allSettled(
      unsyncedMetrics.map((metric) =>
        fetch('/api/health/metrics', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ userId, ...metric })
        })
      )
    );

    const syncedIds: string[] = [];
    results.forEach((result, index) => {
      if (result.status === 'fulfilled' && result.value.ok) {
        syncedIds.push(unsyncedMetrics[index].id);
      }
    });

    return syncedIds;
  }
);

export const healthSlice = createSlice({
  name: 'health',
  initialState,
  reducers: {
    setUserId: (state, action: PayloadAction<string>) => {
      state.userId = action.payload;
    },
    updateProfile: (
      state,
      action: PayloadAction<Partial<HealthState['profile']>>
    ) => {
      if (state.profile) {
        state.profile = { ...state.profile, ...action.payload };
      }
    },
    addMetric: (state, action: PayloadAction<Omit<HealthMetric, 'id' | 'synced'>>) => {
      state.metrics.push({
        ...action.payload,
        id: crypto.randomUUID(),
        synced: false
      });
    },
    setSelectedDate: (state, action: PayloadAction<string>) => {
      state.selectedDate = action.payload;
    },
    clearError: (state) => {
      state.error = null;
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(syncMetrics.pending, (state) => {
        state.syncStatus = 'syncing';
        state.error = null;
      })
      .addCase(syncMetrics.fulfilled, (state, action) => {
        state.syncStatus = 'success';
        state.metrics = state.metrics.map((m) =>
          action.payload.includes(m.id) ? { ...m, synced: true } : m
        );
      })
      .addCase(syncMetrics.rejected, (state, action) => {
        state.syncStatus = 'error';
        state.error = action.error.message || '同步失败';
      });
  }
});

export const {
  setUserId,
  updateProfile,
  addMetric,
  setSelectedDate,
  clearError
} = healthSlice.actions;
Code collapsed

Redux Persist 配置

code
// src/redux/store.ts

import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { hardSet } from 'redux-persist/lib/stateReconciler/hardSet';
import { healthSlice } from './healthSlice';

const persistConfig = {
  key: 'root',
  storage: AsyncStorage,
  stateReconciler: hardSet,
  whitelist: ['userId', 'profile', 'metrics'],
  timeout: null
};

const rootReducer = combineReducers({
  health: persistReducer(persistConfig, healthSlice.reducer)
});

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE']
      }
    })
});

export const persistor = persistStore(store);
Code collapsed

React Native 集成对比

设备连接管理(Zustand)

code
// src/stores/deviceStore.ts

import { create } from 'zustand';
import { BleManager, Device } from 'react-native-ble-plx';

interface DeviceState {
  manager: BleManager | null;
  connectedDevices: Map<string, Device>;
  isScanning: boolean;
  error: string | null;

  init: () => void;
  startScan: () => Promise<void>;
  stopScan: () => void;
  connect: (deviceId: string) => Promise<void>;
  disconnect: (deviceId: string) => Promise<void>;
}

export const useDeviceStore = create<DeviceState>((set, get) => ({
  manager: null,
  connectedDevices: new Map(),
  isScanning: false,
  error: null,

  init: () => {
    const manager = new BleManager();
    set({ manager });

    // 监听设备断开
    manager.onDeviceDisconnected((device) => {
      set((state) => {
        const devices = new Map(state.connectedDevices);
        devices.delete(device.id);
        return { connectedDevices: devices };
      });
    });
  },

  startScan: async () => {
    const { manager } = get();
    if (!manager) return;

    set({ isScanning: true, error: null });

    try {
      await manager.startDeviceScan([], null, (error, device) => {
        if (error) {
          set({ error: error.message, isScanning: false });
          return;
        }

        if (device) {
          set((state) => {
            const devices = new Map(state.connectedDevices);
            devices.set(device.id, device);
            return { connectedDevices: devices };
          });
        }
      });
    } catch (error) {
      set({
        error: error instanceof Error ? error.message : '扫描失败',
        isScanning: false
      });
    }
  },

  stopScan: () => {
    const { manager } = get();
    manager?.stopDeviceScan();
    set({ isScanning: false });
  },

  connect: async (deviceId) => {
    const { manager, connectedDevices } = get();
    if (!manager) return;

    try {
      const device = connectedDevices.get(deviceId) || await manager.device(deviceId);
      await device.connect();

      set((state) => {
        const devices = new Map(state.connectedDevices);
        devices.set(deviceId, device);
        return { connectedDevices: devices };
      });
    } catch (error) {
      set({ error: error instanceof Error ? error.message : '连接失败' });
    }
  },

  disconnect: async (deviceId) => {
    const { connectedDevices } = get();
    const device = connectedDevices.get(deviceId);
    if (device) {
      await device.cancelConnection();
    }
  }
}));
Code collapsed

使用 Redux Saga 处理蓝牙

code
// src/redux/sagas/bleSaga.ts

import { call, put, takeEvery, takeLatest, select } from 'redux-saga/effects';
import { eventChannel } from 'redux-saga';
import BleManager from 'react-native-ble-plx';

import { bleSlice, BleAction } from './bleSlice';

function* initBluetooth() {
  try {
    const manager = new BleManager();
    yield put(bleSlice.actions.setManager(manager));

    // 创建事件通道
    const channel = yield call(createBleEventChannel, manager);

    yield takeEvery(channel, function* (action) {
      yield put(action);
    });
  } catch (error) {
    yield put(
      bleSlice.actions.setError(
        error instanceof Error ? error.message : '初始化失败'
      )
    );
  }
}

function createBleEventChannel(manager: BleManager) {
  return eventChannel((emit) => {
    const unsubscribe = manager.onStateChange((state) => {
      emit(bleSlice.actions.setStateChange(state));
    }, true);

    return () => {
      manager.destroy();
      unsubscribe();
    };
  });
}

function* scanDevices(action: BleAction) {
  try {
    const manager: BleManager = yield select((state) => state.ble.manager);

    yield call([manager, 'startDeviceScan'], [], null, (error, device) => {
      if (error) {
        return put(bleSlice.actions.setScanError(error.message));
      }

      if (device) {
        return put(bleSlice.actions.deviceDiscovered(device));
      }
    });

    yield put(bleSlice.actions.setScanning(true));
  } catch (error) {
    yield put(
      bleSlice.actions.setScanError(
        error instanceof Error ? error.message : '扫描失败'
      )
    );
  }
}

export function* bleSaga() {
  yield takeLatest(bleSlice.actions.init.type, initBluetooth);
  yield takeEvery(bleSlice.actions.startScan.type, scanDevices);
}
Code collapsed

离线同步策略

Zustand 离线队列

code
// src/stores/syncStore.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import NetInfo from '@react-native-community/netinfo';

interface SyncQueueItem {
  id: string;
  timestamp: number;
  endpoint: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: any;
  retries: number;
}

interface SyncState {
  queue: SyncQueueItem[];
  isOnline: boolean;
  isSyncing: boolean;

  initNetworkListener: () => void;
  addToQueue: (item: Omit<SyncQueueItem, 'id' | 'timestamp' | 'retries'>) => void;
  processQueue: () => Promise<void>;
  clearQueue: () => void;
}

export const useSyncStore = create<SyncState>((set, get) => ({
  queue: [],
  isOnline: true,
  isSyncing: false,

  initNetworkListener: () => {
    NetInfo.fetch().then((state) => {
      set({ isOnline: state.isConnected ?? false });
    });

    const unsubscribe = NetInfo.addEventListener((state) => {
      set({ isOnline: state.isConnected ?? false });

      if (state.isConnected) {
        get().processQueue();
      }
    });

    return unsubscribe;
  },

  addToQueue: (item) =>
    set((state) => ({
      queue: [
        ...state.queue,
        {
          ...item,
          id: crypto.randomUUID(),
          timestamp: Date.now(),
          retries: 0
        }
      ]
    })),

  processQueue: async () => {
    const { queue, isOnline } = get();
    if (!isOnline || queue.length === 0) return;

    set({ isSyncing: true });

    const results = await Promise.allSettled(
      queue.map((item) =>
        fetch(item.endpoint, {
          method: item.method,
          headers: { 'Content-Type': 'application/json' },
          body: item.body ? JSON.stringify(item.body) : undefined
        })
      )
    );

    const successfulIds: string[] = [];
    const retryItems: SyncQueueItem[] = [];

    results.forEach((result, index) => {
      const item = queue[index];

      if (result.status === 'fulfilled' && result.value.ok) {
        successfulIds.push(item.id);
      } else {
        // 重试逻辑
        if (item.retries < 3) {
          retryItems.push({ ...item, retries: item.retries + 1 });
        }
      }
    });

    set((state) => ({
      queue: [...retryItems, ...state.queue.filter((q) => !successfulIds.includes(q.id))],
      isSyncing: false
    }));
  },

  clearQueue: () => set({ queue: [] })
}));
Code collapsed

性能优化对比

Zustand 选择器优化

code
// Zustand 自动优化
const steps = useHealthStore((state) =>
  state.metrics.filter(m => m.type === 'steps')
);

// 使用浅比较
import { shallow } from 'zustand/shallow';

const { metrics, syncStatus } = useHealthStore(
  (state) => ({ metrics: state.metrics, syncStatus: state.syncStatus }),
  shallow
);
Code collapsed

Redux 选择器优化

code
// 使用 Reselect 创建记忆化选择器
import { createSelector } from '@reduxjs/toolkit';

const selectMetrics = (state: RootState) => state.health.metrics;
const selectSelectedDate = (state: RootState) => state.health.selectedDate;

const selectTodayMetrics = createSelector(
  [selectMetrics, selectSelectedDate],
  (metrics, selectedDate) => {
    const date = new Date(selectedDate);
    return metrics.filter(m => {
      const metricDate = new Date(m.timestamp);
      return (
        metricDate.getDate() === date.getDate() &&
        metricDate.getMonth() === date.getMonth() &&
        metricDate.getFullYear() === date.getFullYear()
      );
    });
  }
);

// 在组件中使用
const todayMetrics = useAppSelector(selectTodayMetrics);
Code collapsed

总结建议

场景推荐方案
小型健康应用Zustand
简单状态管理Zustand
需要快速开发Zustand
大型企业应用Redux + Redux Toolkit
复杂异步流程Redux + Saga
需要时间旅行调试Redux DevTools

参考资料

#

文章标签

react-native
状态管理
zustand
redux
健康科技
移动应用

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅