康心伴Logo
康心伴WellAlly
前端开发

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

5 分钟阅读

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

概述

状态管理是 React 健康应用的核心挑战之一。用户数据、健康指标、应用设置等需要跨组件共享和持久化。本文将深入对比两种流行的状态管理方案:Zustand 和 Redux Toolkit (RTK)。

方案对比总览

特性ZustandRedux 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 的情况

  1. 小型到中型应用
  2. 希望减少样板代码
  3. 团队不熟悉 Redux
  4. 需要简单的持久化
  5. 关注 bundle 大小

选择 Redux Toolkit 的情况

  1. 大型企业级应用
  2. 需要强大的 DevTools
  3. 团队有 Redux 经验
  4. 需要中间件生态
  5. 需要时间旅行调试

参考资料

#

文章标签

react
状态管理
zustand
redux-toolkit
健康科技

觉得这篇文章有帮助?

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