Zustand vs Redux Toolkit:React 健康应用状态管理对比
概述
状态管理是 React 健康应用的核心挑战之一。用户数据、健康指标、应用设置等需要跨组件共享和持久化。本文将深入对比两种流行的状态管理方案:Zustand 和 Redux Toolkit (RTK)。
方案对比总览
| 特性 | Zustand | Redux Toolkit |
|---|---|---|
| 学习曲线 | 低 | 中等 |
| Bundle 大小 | ~1KB | ~12KB |
| 样板代码 | 最少 | 较少(相比传统 Redux) |
| DevTools | 支持(需插件) | 内置强大 DevTools |
| 中间件 | 简单 | 丰富 |
| TypeScript 支持 | 优秀 | 优秀 |
| 社区生态 | 较新 | 成熟 |
Zustand 实践
基础设置
code
// stores/healthStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
// 类型定义
interface HealthMetric {
id: string;
timestamp: Date;
type: 'heartRate' | 'bloodPressure' | 'steps' | 'sleep';
value: number;
unit: string;
}
interface UserProfile {
id: string;
name: string;
age: number;
weight: number;
height: number;
targetSteps: number;
}
interface HealthState {
// 用户数据
user: UserProfile | null;
// 健康数据
metrics: HealthMetric[];
// UI 状态
selectedDate: Date;
isLoading: boolean;
error: string | null;
// Actions
setUser: (user: UserProfile) => void;
addMetric: (metric: Omit<HealthMetric, 'id'>) => void;
updateMetric: (id: string, value: number) => void;
deleteMetric: (id: string) => void;
setSelectedDate: (date: Date) => void;
fetchMetrics: (startDate: Date, endDate: Date) => Promise<void>;
clearError: () => void;
}
// 创建 store
export const useHealthStore = create<HealthState>()(
persist(
(set, get) => ({
// 初始状态
user: null,
metrics: [],
selectedDate: new Date(),
isLoading: false,
error: null,
// Actions
setUser: (user) => set({ user }),
addMetric: (metric) => set((state) => ({
metrics: [
...state.metrics,
{ ...metric, id: crypto.randomUUID() }
]
})),
updateMetric: (id, value) => set((state) => ({
metrics: state.metrics.map(m =>
m.id === id ? { ...m, value } : m
)
})),
deleteMetric: (id) => set((state) => ({
metrics: state.metrics.filter(m => m.id !== id)
})),
setSelectedDate: (date) => set({ selectedDate: date }),
fetchMetrics: async (startDate, endDate) => {
set({ isLoading: true, error: null });
try {
const response = await fetch(
`/api/health/metrics?start=${startDate.toISOString()}&end=${endDate.toISOString()}`
);
if (!response.ok) throw new Error('获取数据失败');
const data = await response.json();
set({ metrics: data, isLoading: false });
} catch (error) {
set({
error: error instanceof Error ? error.message : '未知错误',
isLoading: false
});
}
},
clearError: () => set({ error: null })
}),
{
name: 'health-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
user: state.user,
metrics: state.metrics
})
}
)
);
// 选择器 hooks
export const useUser = () => useHealthStore((state) => state.user);
export const useMetrics = () => useHealthStore((state) => state.metrics);
export const useSelectedDate = () => useHealthStore((state) => state.selectedDate);
export const useHealthActions = () => useHealthStore((state) => ({
setUser: state.setUser,
addMetric: state.addMetric,
updateMetric: state.updateMetric,
deleteMetric: state.deleteMetric,
setSelectedDate: state.setSelectedDate,
fetchMetrics: state.fetchMetrics,
clearError: state.clearError
}));
Code collapsed
组件中使用
code
// components/HealthDashboard.tsx
import React, { useEffect } from 'react';
import { useHealthStore, useUser, useMetrics, useHealthActions } from '@/stores/healthStore';
export const HealthDashboard: React.FC = () => {
const user = useUser();
const metrics = useMetrics();
const selectedDate = useHealthStore((state) => state.selectedDate);
const isLoading = useHealthStore((state) => state.isLoading);
const error = useHealthStore((state) => state.error);
const { setSelectedDate, fetchMetrics, clearError } = useHealthActions();
useEffect(() => {
const startOfDay = new Date(selectedDate);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(selectedDate);
endOfDay.setHours(23, 59, 59, 999);
fetchMetrics(startOfDay, endOfDay);
}, [selectedDate, fetchMetrics]);
const todayMetrics = metrics.filter(
m => new Date(m.timestamp).toDateString() === selectedDate.toDateString()
);
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorBanner message={error} onDismiss={clearError} />;
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">健康仪表板</h1>
<DatePicker
value={selectedDate}
onChange={setSelectedDate}
/>
</div>
{user && (
<div className="bg-white rounded-lg shadow p-4">
<h2 className="font-semibold mb-2">欢迎, {user.name}</h2>
<div className="grid grid-cols-3 gap-4">
<div>
<span className="text-gray-600">年龄</span>
<p className="font-semibold">{user.age} 岁</p>
</div>
<div>
<span className="text-gray-600">体重</span>
<p className="font-semibold">{user.weight} kg</p>
</div>
<div>
<span className="text-gray-600">身高</span>
<p className="font-semibold">{user.height} cm</p>
</div>
</div>
</div>
)}
<MetricsGrid metrics={todayMetrics} />
</div>
);
};
Code collapsed
Redux Toolkit 实践
基础设置
code
// stores/healthSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
// 类型定义
interface HealthMetric {
id: string;
timestamp: string;
type: 'heartRate' | 'bloodPressure' | 'steps' | 'sleep';
value: number;
unit: string;
}
interface UserProfile {
id: string;
name: string;
age: number;
weight: number;
height: number;
targetSteps: number;
}
interface HealthState {
user: UserProfile | null;
metrics: HealthMetric[];
selectedDate: string;
isLoading: boolean;
error: string | null;
}
const initialState: HealthState = {
user: null,
metrics: [],
selectedDate: new Date().toISOString(),
isLoading: false,
error: null
};
// Async thunks
export const fetchMetrics = createAsyncThunk(
'health/fetchMetrics',
async ({ startDate, endDate }: { startDate: string; endDate: string }) => {
const response = await fetch(
`/api/health/metrics?start=${startDate}&end=${endDate}`
);
if (!response.ok) throw new Error('获取数据失败');
return response.json() as Promise<HealthMetric[]>;
}
);
// Slice
export const healthSlice = createSlice({
name: 'health',
initialState,
reducers: {
setUser: (state, action: PayloadAction<UserProfile>) => {
state.user = action.payload;
},
addMetric: (state, action: PayloadAction<Omit<HealthMetric, 'id'>>) => {
state.metrics.push({
...action.payload,
id: crypto.randomUUID()
});
},
updateMetric: (
state,
action: PayloadAction<{ id: string; value: number }>
) => {
const metric = state.metrics.find(m => m.id === action.payload.id);
if (metric) {
metric.value = action.payload.value;
}
},
deleteMetric: (state, action: PayloadAction<string>) => {
state.metrics = state.metrics.filter(m => m.id !== action.payload);
},
setSelectedDate: (state, action: PayloadAction<string>) => {
state.selectedDate = action.payload;
},
clearError: (state) => {
state.error = null;
}
},
extraReducers: (builder) => {
builder
.addCase(fetchMetrics.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchMetrics.fulfilled, (state, action) => {
state.isLoading = false;
state.metrics = action.payload;
})
.addCase(fetchMetrics.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error.message || '获取数据失败';
});
}
});
export const {
setUser,
addMetric,
updateMetric,
deleteMetric,
setSelectedDate,
clearError
} = healthSlice.actions;
Code collapsed
Store 配置
code
// store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { healthSlice } from './stores/healthSlice';
const persistConfig = {
key: 'health-root',
storage,
whitelist: ['user', 'metrics']
};
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);
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Code collapsed
类型安全的 Hooks
code
// hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Code collapsed
组件中使用
code
// components/HealthDashboard.tsx
import React, { useEffect } from 'react';
import { useAppDispatch, useAppSelector } from '@/hooks';
import {
fetchMetrics,
setSelectedDate,
clearError
} from '@/stores/healthSlice';
export const HealthDashboard: React.FC = () => {
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.health.user);
const metrics = useAppSelector((state) => state.health.metrics);
const selectedDate = useAppSelector((state) => state.health.selectedDate);
const isLoading = useAppSelector((state) => state.health.isLoading);
const error = useAppSelector((state) => state.health.error);
useEffect(() => {
const date = new Date(selectedDate);
const startOfDay = new Date(date.setHours(0, 0, 0, 0)).toISOString();
const endOfDay = new Date(date.setHours(23, 59, 59, 999)).toISOString();
dispatch(fetchMetrics({ startDate: startOfDay, endDate: endOfDay }));
}, [selectedDate, dispatch]);
const handleDateChange = (date: Date) => {
dispatch(setSelectedDate(date.toISOString()));
};
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorBanner message={error} onDismiss={() => dispatch(clearError())} />;
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">健康仪表板</h1>
<DatePicker
value={new Date(selectedDate)}
onChange={handleDateChange}
/>
</div>
{user && <UserProfileCard user={user} />}
<MetricsGrid metrics={metrics} />
</div>
);
};
Code collapsed
高级模式对比
Zustand 高级用法
code
// stores/advancedHealthStore.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
// 组合多个 stores
interface BearState {
bears: number;
increase: () => void;
}
interface FoodState {
fish: number;
addFish: (amount: number) => void;
}
// 使用 immer 中间件
import { immer } from 'zustand/middleware/immer';
export const useHealthStore = create(
devtools(
persist(
immer((set) => ({
// 复杂嵌套状态更新
user: {
profile: null,
preferences: {
theme: 'light',
notifications: true
}
},
updateUserProfile: (profile) =>
set((state) => {
state.user.profile = profile;
}),
toggleNotifications: () =>
set((state) => {
state.user.preferences.notifications =
!state.user.preferences.notifications;
})
})),
{ name: 'health-store' }
)
)
);
// 订阅特定状态变化
useHealthStore.subscribe(
(state) => state.user,
(user) => {
console.log('用户变化:', user);
}
);
Code collapsed
Redux Toolkit 高级用法
code
// stores/advancedHealthSlice.ts
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
// 使用 EntityAdapter 处理集合
const metricsAdapter = createEntityAdapter<HealthMetric>({
sortComparer: (a, b) => a.timestamp.localeCompare(b.timestamp)
});
export const healthSlice = createSlice({
name: 'health',
initialState: metricsAdapter.getInitialState<{
selectedDate: string;
isLoading: boolean;
error: string | null;
}>({
selectedDate: new Date().toISOString(),
isLoading: false,
error: null
}),
reducers: {
// 使用 adapter 提供的 reducers
metricAdded: metricsAdapter.addOne,
metricsReceived: metricsAdapter.setAll,
metricUpdated: metricsAdapter.updateOne,
metricRemoved: metricsAdapter.removeOne,
setSelectedDate: (state, action) => {
state.selectedDate = action.payload;
}
}
});
// 导出选择器
export const {
selectAll: selectAllMetrics,
selectById: selectMetricById,
selectEntities: selectMetricEntities
} = metricsAdapter.getSelectors((state: RootState) => state.health);
Code collapsed
何时选择哪个?
选择 Zustand 的情况
- 小型到中型应用
- 希望减少样板代码
- 团队不熟悉 Redux
- 需要简单的持久化
- 关注 bundle 大小
选择 Redux Toolkit 的情况
- 大型企业级应用
- 需要强大的 DevTools
- 团队有 Redux 经验
- 需要中间件生态
- 需要时间旅行调试