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 |