State Management in React: Comparing Redux, MobX, and Zustand (With TypeScript, Unit Tests, Performance & Memory Benchmarks)

Ram Kumar

Ram Kumar

January 24, 20253 min read

State Management in React: Comparing Redux, MobX, and Zustand (With TypeScript, Unit Tests, Performance & Memory Benchmarks)

State management is a crucial part of React applications, ensuring data consistency across components. In this guide, we will compare Redux, MobX, and Zustand—exploring their advantages, disadvantages, and implementations in TypeScript with unit tests using Jest and React Testing Library, along with performance benchmarks, memory usage comparisons, and integration tests.

We’ll also build a sample banking app to demonstrate how each library works in practice, complete with unit tests and performance measurements.

Performance Benchmarks

Benchmarking Setup

To compare the performance of Redux, MobX, and Zustand, we measure their execution time for state updates under different loads. We use performance.now() to track the time taken for operations.

Performance Test Code

function measurePerformance(callback: () => void, iterations = 10000) {
  const start = performance.now();
  for (let i = 0; i < iterations; i++) {
    callback();
  }
  const end = performance.now();
  return end - start;
}

Redux Performance

const reduxTime = measurePerformance(() => {
  store.dispatch(deposit(10));
  store.dispatch(withdraw(10));
});
console.log(`Redux Execution Time: ${reduxTime}ms`);

MobX Performance

const mobxTime = measurePerformance(() => {
  accountStore.deposit(10);
  accountStore.withdraw(10);
});
console.log(`MobX Execution Time: ${mobxTime}ms`);

Zustand Performance

const zustandTime = measurePerformance(() => {
  useAccountStore.getState().deposit(10);
  useAccountStore.getState().withdraw(10);
});
console.log(`Zustand Execution Time: ${zustandTime}ms`);

Memory Usage Benchmarks

Memory consumption is another crucial factor in state management. We measure memory usage using the Performance API and the Heap Snapshot Tool in Chrome DevTools.

Memory Usage Test Code

function measureMemoryUsage() {
  if (performance.memory) {
    console.log(`Heap Used: ${performance.memory.usedJSHeapSize / 1024 / 1024} MB`);
  } else {
    console.warn("Memory API not supported");
  }
}

Results depend on application complexity and state size.

Observations:

  • Redux: Uses more memory due to immutable state updates and history tracking.
  • MobX: Consumes less memory but may hold onto objects longer due to observables.
  • Zustand: Has the smallest memory footprint due to its minimalist approach.

Sample Banking App Code

To demonstrate how each library works in practice, we’ll build a simple banking app that allows users to deposit and withdraw money. The app will be implemented using Redux, MobX, and Zustand, with TypeScript support and unit tests.

1. Redux Implementation

State and Actions

// store/accountSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface AccountState {
  balance: number;
}

const initialState: AccountState = {
  balance: 1000,
};

const accountSlice = createSlice({
  name: 'account',
  initialState,
  reducers: {
    deposit(state, action: PayloadAction<number>) {
      state.balance += action.payload;
    },
    withdraw(state, action: PayloadAction<number>) {
      state.balance -= action.payload;
    },
  },
});

export const { deposit, withdraw } = accountSlice.actions;
export default accountSlice.reducer;

Store Setup

// store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import accountReducer from './accountSlice';

const store = configureStore({
  reducer: {
    account: accountReducer,
  },
});

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

Component

// App.tsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from './store/store';
import { deposit, withdraw } from './store/accountSlice';

const App: React.FC = () => {
  const balance = useSelector((state: RootState) => state.account.balance);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Balance: ${balance}</h1>
      <button onClick={() => dispatch(deposit(100))}>Deposit $100</button>
      <button onClick={() => dispatch(withdraw(50))}>Withdraw $50</button>
    </div>
  );
};

export default App;

Unit Tests

// App.test.tsx
import { render, fireEvent, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import store from './store/store';
import App from './App';

test('Redux: Deposit and Withdraw updates balance', () => {
  render(
    <Provider store={store}>
      <App />
    </Provider>
  );

  expect(screen.getByText(/Balance: 1000/)).toBeInTheDocument();

  fireEvent.click(screen.getByText('Deposit $100'));
  expect(screen.getByText(/Balance: 1100/)).toBeInTheDocument();

  fireEvent.click(screen.getByText('Withdraw $50'));
  expect(screen.getByText(/Balance: 1050/)).toBeInTheDocument();
});

2. MobX Implementation

Store

// store/AccountStore.ts
import { makeAutoObservable } from 'mobx';

class AccountStore {
  balance = 1000;

  constructor() {
    makeAutoObservable(this);
  }

  deposit(amount: number) {
    this.balance += amount;
  }

  withdraw(amount: number) {
    this.balance -= amount;
  }
}

const accountStore = new AccountStore();
export default accountStore;

Component

// App.tsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import accountStore from './store/AccountStore';

const App: React.FC = observer(() => {
  return (
    <div>
      <h1>Balance: ${accountStore.balance}</h1>
      <button onClick={() => accountStore.deposit(100)}>Deposit $100</button>
      <button onClick={() => accountStore.withdraw(50)}>Withdraw $50</button>
    </div>
  );
});

export default App;

Unit Tests

// App.test.tsx
import { render, fireEvent, screen } from '@testing-library/react';
import App from './App';
import accountStore from './store/AccountStore';

beforeEach(() => {
  accountStore.balance = 1000; // Reset state before each test
});

test('MobX: Deposit and Withdraw updates balance', () => {
  render(<App />);

  expect(screen.getByText(/Balance: 1000/)).toBeInTheDocument();

  fireEvent.click(screen.getByText('Deposit $100'));
  expect(screen.getByText(/Balance: 1100/)).toBeInTheDocument();

  fireEvent.click(screen.getByText('Withdraw $50'));
  expect(screen.getByText(/Balance: 1050/)).toBeInTheDocument();
});

3. Zustand Implementation

Store

// store/useAccountStore.ts
import create from 'zustand';

interface AccountState {
  balance: number;
  deposit: (amount: number) => void;
  withdraw: (amount: number) => void;
}

const useAccountStore = create<AccountState>((set) => ({
  balance: 1000,
  deposit: (amount) => set((state) => ({ balance: state.balance + amount })),
  withdraw: (amount) => set((state) => ({ balance: state.balance - amount })),
}));

export default useAccountStore;

Component

// App.tsx
import React from 'react';
import useAccountStore from './store/useAccountStore';

const App: React.FC = () => {
  const { balance, deposit, withdraw } = useAccountStore();

  return (
    <div>
      <h1>Balance: ${balance}</h1>
      <button onClick={() => deposit(100)}>Deposit $100</button>
      <button onClick={() => withdraw(50)}>Withdraw $50</button>
    </div>
  );
};

export default App;

Unit Tests

// App.test.tsx
import { render, fireEvent, screen } from '@testing-library/react';
import App from './App';
import useAccountStore from './store/useAccountStore';

beforeEach(() => {
  useAccountStore.setState({ balance: 1000 }); // Reset state before each test
});

test('Zustand: Deposit and Withdraw updates balance', () => {
  render(<App />);

  expect(screen.getByText(/Balance: 1000/)).toBeInTheDocument();

  fireEvent.click(screen.getByText('Deposit $100'));
  expect(screen.getByText(/Balance: 1100/)).toBeInTheDocument();

  fireEvent.click(screen.getByText('Withdraw $50'));
  expect(screen.getByText(/Balance: 1050/)).toBeInTheDocument();
});

Conclusion

Key Takeaways

  • Redux: Best for large-scale applications with complex state management needs. It provides predictability and debugging tools but requires more boilerplate.
  • MobX: Ideal for reactive state management with minimal boilerplate. It’s flexible and performant but may hold onto memory longer.
  • Zustand: A lightweight and simple solution for smaller projects or when performance and ease of use are priorities.

When to Use Which?

  • Use Redux for enterprise-level applications with complex state logic.
  • Use MobX for applications requiring reactivity and flexibility.
  • Use Zustand for smaller projects or when you want a minimalistic state management solution.

Final Thoughts

Each library has its strengths and trade-offs. Choose based on your project’s requirements, team familiarity, and long-term maintainability. The sample banking app and unit tests provided above should help you get started with any of these libraries in a TypeScript-based React application.

Previous: Best Practices for Building Applications with RemixJS and TypeScript
Next: Building Forms with Remix.js and TypeScript