WellAlly Logo
WellAlly康心伴
Development

Zustand vs. Redux Toolkit: Choosing a State Manager for Your React Health App

A technical deep-dive comparing Zustand and Redux Toolkit for React. We analyze boilerplate, complex data handling, performance, and offline sync for health app development.

W
2025-12-11
9 min read

Building a modern health dashboard in React requires careful consideration of state management. You're not just dealing with simple UI toggles; you're handling sensitive user data, complex data structures from wearables, and the critical need for features like offline access. Choosing the right state management library is a foundational decision that will impact your app's performance, scalability, and maintainability.

In this article, we'll dive into a technical comparison of two of the most popular state management solutions in the React ecosystem: Zustand and Redux Toolkit (RTK). We'll analyze them through the lens of building a health dashboard, focusing on:

  • Boilerplate and Developer Experience: How quickly can you get up and running?
  • Managing Complex Health Data: How do they handle normalized and relational data?
  • Performance: Which is better for real-time updates from health devices?
  • Offline Synchronization: How can we implement an "offline-first" approach for our health app?

By the end, you'll have a clear understanding of the strengths and weaknesses of each library, complete with practical code examples, to help you make an informed decision for your next health tech project.

Understanding the Problem: State Management in a Health App

A health dashboard presents unique state management challenges:

  • Complex, Normalized Data: We need to manage user profiles, a collection of connected devices, and time-series data for various health metrics (heart rate, steps, sleep, etc.).
  • Real-time Updates: The app might need to handle frequent updates from a connected wearable via WebSockets or similar technology.
  • Offline Functionality: A user should be able to input data or view their existing data even without an internet connection. The app must sync seamlessly when it comes back online.
  • Performance: With potentially large datasets and real-time updates, we need to avoid unnecessary re-renders to keep the UI smooth and responsive.

Prerequisites

  • A solid understanding of React and hooks.
  • Node.js and npm/yarn installed.
  • Familiarity with basic state management concepts in React.

Let's set up a new React project to build our examples:

code
npx create-react-app health-app --template typescript
cd health-app
Code collapsed

Now, let's look at how Zustand and Redux Toolkit tackle these challenges.


Step 1: Boilerplate and Initial Setup

What we're doing

We'll create a simple store with both libraries to manage a user's profile information. This will give us a first look at the difference in boilerplate and setup.

Zustand: Minimalist and Hook-Based

Zustand is known for its simplicity and minimal API. You can create a global store with just a few lines of code.

Installation:

code
npm install zustand
Code collapsed

Creating the user store (src/stores/userStore.ts):

code
// src/stores/userStore.ts
import { create } from 'zustand';

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

interface UserState {
  profile: UserProfile | null;
  setUserProfile: (profile: UserProfile) => void;
  clearUserProfile: () => void;
}

export const useUserStore = create<UserState>((set) => ({
  profile: null,
  setUserProfile: (profile) => set({ profile }),
  clearUserProfile: () => set({ profile: null }),
}));
Code collapsed

Redux Toolkit: Structured and Opinionated

Redux Toolkit (RTK) is the official, recommended way to write Redux logic. It simplifies the traditionally verbose Redux setup with tools like configureStore and createSlice.

Installation:

code
npm install @reduxjs/toolkit react-redux
Code collapsed

Creating the user slice (src/features/user/userSlice.ts):

code
// src/features/user/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../../app/store';

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

interface UserState {
  profile: UserProfile | null;
}

const initialState: UserState = {
  profile: null,
};

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    setUserProfile: (state, action: PayloadAction<UserProfile>) => {
      state.profile = action.payload;
    },
    clearUserProfile: (state) => {
      state.profile = null;
    },
  },
});

export const { setUserProfile, clearUserProfile } = userSlice.actions;

export const selectUserProfile = (state: RootState) => state.user.profile;

export default userSlice.reducer;
Code collapsed

Configuring the store (src/app/store.ts):

code
// src/app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from '../features/user/userSlice';

export const store = configureStore({
  reducer: {
    user: userReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Code collapsed

How it works

  • Zustand: Creates a custom hook (useUserStore) directly. It doesn't require a context provider to wrap your application. State updates are done via the set function provided in the create callback.
  • Redux Toolkit: Uses createSlice to generate actions and a reducer for a specific piece of state. These reducers are then combined in a single global store using configureStore. Your application needs to be wrapped in a <Provider> component.

Verdict on Boilerplate

Zustand is the clear winner in terms of minimal boilerplate. The setup is faster and requires fewer files and concepts to get started. Redux Toolkit, while much improved over classic Redux, still has more "ceremony" with its slice and store configuration.


Step 2: Managing Complex Data (Health Metrics)

What we're doing

Our health app needs to manage a collection of health metrics, which can be thought of as a normalized dataset. We'll see how both libraries handle this common scenario.

Redux Toolkit: createEntityAdapter for Normalization

RTK shines when it comes to managing normalized data. createEntityAdapter is a powerful utility that provides pre-built reducers and selectors for CRUD operations on a normalized state structure.

Creating the metrics slice (src/features/metrics/metricsSlice.ts):

code
// src/features/metrics/metricsSlice.ts
import {
  createSlice,
  createEntityAdapter,
  PayloadAction,
} from '@reduxjs/toolkit';
import type { RootState } from '../../app/store';

export interface HealthMetric {
  id: string; // e.g., 'heart-rate-2023-12-25T10:00:00Z'
  type: 'heartRate' | 'steps' | 'sleep';
  value: number;
  timestamp: string;
}

const metricsAdapter = createEntityAdapter<HealthMetric>({
  sortComparer: (a, b) => b.timestamp.localeCompare(a.timestamp),
});

const initialState = metricsAdapter.getInitialState({
  status: 'idle',
  error: null as string | null,
});

const metricsSlice = createSlice({
  name: 'metrics',
  initialState,
  reducers: {
    addMetric: metricsAdapter.addOne,
    addMetrics: metricsAdapter.addMany,
    updateMetric: metricsAdapter.updateOne,
  },
});

export const { addMetric, addMetrics, updateMetric } = metricsSlice.actions;

export const {
  selectAll: selectAllMetrics,
  selectById: selectMetricById,
} = metricsAdapter.getSelectors((state: RootState) => state.metrics);

export default metricsSlice.reducer;
Code collapsed

Zustand: Using Slices for Modularity

While Zustand doesn't have a built-in equivalent to createEntityAdapter, you can achieve a similar level of organization by using the "slice pattern". This involves creating separate slices of state and combining them into a single store.

Creating a metrics store with slices (src/stores/rootStore.ts):

code
// src/stores/rootStore.ts
import { create } from 'zustand';
import { UserState, useUserStore } from './userStore';

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

interface MetricsState {
  metrics: Record<string, HealthMetric>;
  addMetric: (metric: HealthMetric) => void;
  // ... other metric actions
}

const createMetricsSlice = (set: any) => ({
  metrics: {},
  addMetric: (metric: HealthMetric) =>
    set((state: { metrics: { metrics: any; }; }) => ({
      metrics: {
        ...state.metrics.metrics,
        [metric.id]: metric,
      },
    })),
});

// We can combine slices, although for this example we'll keep them separate
// to show how Zustand encourages modularity.
// const useRootStore = create((...a) => ({
//   ...useUserStore(...a),
//   ...createMetricsSlice(...a),
// }));

// For simplicity, we'll create a separate metrics store
export const useMetricsStore = create<MetricsState>(createMetricsSlice);

Code collapsed

How it works

  • Redux Toolkit: createEntityAdapter automatically generates the state shape ({ ids: [], entities: {} }) and reducers like addOne, addMany, updateOne, which simplifies working with collections of data.
  • Zustand: You have to define the state shape and update logic yourself. While more manual, it gives you complete flexibility. The slice pattern helps keep your store organized as it grows.

Verdict on Complex Data

Redux Toolkit has a clear advantage here for highly structured, relational data. createEntityAdapter enforces best practices for data normalization and saves you from writing repetitive CRUD logic. Zustand is perfectly capable, but requires you to build this logic yourself.


Step 3: Implementing Offline Sync

A key feature for a health app is the ability to work offline. Both libraries can achieve this with middleware that persists the store's state to local storage.

Zustand: persist Middleware

Zustand offers a clean and simple persist middleware.

Updating the user store for persistence (src/stores/userStore.ts):

code
// src/stores/userStore.ts
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

// ... (interfaces remain the same)

export const useUserStore = create(
  persist<UserState>(
    (set) => ({
      profile: null,
      setUserProfile: (profile) => set({ profile }),
      clearUserProfile: () => set({ profile: null }),
    }),
    {
      name: 'user-storage', // unique name
      storage: createJSONStorage(() => localStorage), // (optional) default: localStorage
    }
  )
);
Code collapsed

Redux Toolkit: redux-persist

For Redux Toolkit, the most common solution is the redux-persist library.

Installation:

code
npm install redux-persist
Code collapsed

Updating the store configuration (src/app/store.ts):

code
// src/app/store.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import userReducer from '../features/user/userSlice';
import metricsReducer from '../features/metrics/metricsSlice';

const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['user'], // only persist the user slice
};

const rootReducer = combineReducers({
  user: userReducer,
  metrics: metricsReducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false, // Recommended to disable for redux-persist
    }),
});

export const persistor = persistStore(store);

// ... (RootState and AppDispatch types)```

**Updating the root component (`src/index.tsx`):**

```tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { store, persistor } from './app/store';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);
Code collapsed

Verdict on Offline Sync

Zustand's built-in persist middleware is more straightforward to implement. It requires less configuration and no changes to your application's root component. redux-persist is more powerful, offering more advanced options like migrations and transforms, but it also adds more complexity to your setup.


Performance Considerations

  • Bundle Size: Zustand is significantly smaller than Redux Toolkit, which can be a factor in performance-critical applications.
  • Re-renders: Zustand's hook-based selectors make it easy to subscribe to specific parts of the state, which can prevent unnecessary re-renders by default. In Redux, while useSelector is powerful, it's up to the developer to ensure they are selecting the minimal state necessary to avoid extra renders. Libraries like reselect are often used with Redux to create memoized selectors for performance optimization.

For a health dashboard with potentially high-frequency updates (e.g., from a real-time heart rate monitor), Zustand's performance characteristics might give it a slight edge due to its fine-grained subscription model.

Conclusion: Which One Should You Choose?

Both Zustand and Redux Toolkit are excellent, mature libraries for state management in React. The best choice depends on the specific needs of your project and team.

Choose Zustand if:

  • You are working on a small to medium-sized project.
  • You prioritize simplicity, speed of development, and minimal boilerplate.
  • You want a lightweight solution with a small bundle size.
  • Your team prefers a more flexible, less opinionated approach.

Choose Redux Toolkit if:

  • You are building a large-scale, enterprise-level application.
  • Your team needs a predictable, structured approach to state management.
  • You need powerful developer tools, like the Redux DevTools, for time-travel debugging.
  • You have a lot of complex, normalized data that would benefit from createEntityAdapter.

For our hypothetical health dashboard, if you're a startup building an MVP, Zustand would be an excellent choice to get up and running quickly. If you're building a large, complex health platform with multiple teams, the structure and predictability of Redux Toolkit would be more beneficial in the long run.

Resources

#

Article Tags

reactjavascriptstatemanagementhealthtechtutorial
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare TechnologySoftware DevelopmentUser ExperienceAI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey

© 2024 康心伴 WellAlly · Professional Health Management