Skip to main content
  1. Frontends/
  2. React Guides/

State of React State: Redux Toolkit vs. Zustand vs. Signals

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect.

If you’ve been in the React ecosystem for more than a week, you’ve heard the argument. “Redux is dead,” they said in 2018. “Context is all you need,” they claimed in 2020. Yet, here we are. It’s 2026, and the battlefield of state management has shifted from “how do we pass data” to “how do we prevent re-renders.”

The React landscape has matured. With React 19 stabilizing and the React Compiler (formerly React Forget) handling memoization automatically, the criteria for choosing a state library have changed. We aren’t just looking for a place to store JSON anymore; we are looking for fine-grained reactivity, minimal boilerplate, and TypeScript inference that doesn’t require a Ph.D. in Generics.

Today, we are putting three distinct paradigms in the ring:

  1. Redux Toolkit (RTK): The enterprise standard that reinvented itself.
  2. Zustand: The minimalist darling that stripped away the fluff.
  3. Signals: The performance-obsessed challenger (borrowing from SolidJS and Preact) that questions the Virtual DOM model itself.

This isn’t a “Hello World” tutorial. This is an architectural deep dive for developers who ship to production.

Prerequisites & Environment
#

Before we tear these libraries apart, let’s set the stage. We assume you are running a modern stack. If you are still on Class Components, this guide might feel like science fiction.

The 2026 Standard Stack:

  • Node.js: v22+ (LTS)
  • React: v19+
  • Language: TypeScript 5.x (Strict Mode)
  • Build Tool: Vite 6+ or Turbopack

You won’t need a specific backend; we will simulate async states. Ensure your environment is ready:

# Create a fresh playground
npm create vite@latest state-wars -- --template react-ts
cd state-wars
npm install

1. The Heavyweight: Redux Toolkit (RTK)
#

Let’s address the elephant in the room. Redux used to be synonymous with boilerplate. Switch statements, string constants, and manual immutability were a nightmare.

But if you haven’t looked at Redux since 2022, you’re judging a ghost. Redux Toolkit (RTK) is concise, opinionated, and standardized. It remains the safest bet for large teams where strict architectural patterns prevent the “Wild West” of code styles.

The Modern Redux Setup
#

We don’t do switch statements anymore. We use createSlice.

Step 1: Install Dependencies

npm install @reduxjs/toolkit react-redux

Step 2: The Slice (Domain Logic)

Here is a store managing a user’s session and theme preference. Notice the direct mutation logic—thanks, Immer.

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

interface UserState {
  isAuthenticated: boolean;
  username: string | null;
  theme: 'light' | 'dark';
}

const initialState: UserState = {
  isAuthenticated: false,
  username: null,
  theme: 'light',
};

export const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    login: (state, action: PayloadAction<string>) => {
      // Direct mutation! Immer handles the immutable update behind the scenes.
      state.username = action.payload;
      state.isAuthenticated = true;
    },
    logout: (state) => {
      state.username = null;
      state.isAuthenticated = false;
    },
    toggleTheme: (state) => {
      state.theme = state.theme === 'light' ? 'dark' : 'light';
    },
  },
});

export const { login, logout, toggleTheme } = userSlice.actions;
export default userSlice.reducer;

Step 3: The Store & Hooks

Types are inferred automatically. This is where RTK shines—you write this once, and your whole app is type-safe.

// src/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './userSlice';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

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

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

// Custom hooks for consumption
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

The Verdict on RTK: It’s robust. The DevTools are unbeatable (time-travel debugging is still a killer feature for complex flows). However, wrapping your app in a <Provider> and the mental overhead of “Dispatching Actions” can feel heavy for simpler apps.

2. The Minimalist: Zustand
#

Zustand (German for “State”) took the React world by storm by asking: “What if Redux didn’t hurt?”

It removes the Provider wrapper hell. It treats state as a hook. It’s un-opinionated. For 80% of applications in 2026, Zustand is likely the default choice because it gets out of your way.

The Hook-Based Store
#

Step 1: Install Dependencies

npm install zustand

Step 2: Create the Store

That’s it. No reducers, no slices folder structure (unless you want one).

// src/store/useBoundStore.ts
import { create } from 'zustand';

interface AppState {
  bears: number;
  increasePopulation: () => void;
  removeAllBears: () => void;
  // Async actions are just async functions
  fetchBears: () => Promise<void>;
}

export const useStore = create<AppState>((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  
  fetchBears: async () => {
    // Simulate API
    const response = await new Promise<number>(r => setTimeout(() => r(10), 500));
    set({ bears: response });
  },
}));

Step 3: Consumption

This is where Zustand wins on performance. You select only what you need. If increasePopulation changes (it won’t, it’s stable) or other unrelated state changes, this component won’t re-render unless bears changes.

import { useStore } from './store/useBoundStore';

const BearCounter = () => {
  // Selector pattern prevents unnecessary re-renders
  const bears = useStore((state) => state.bears);
  return <h1>{bears} around here ...</h1>;
};

const Controls = () => {
  const increasePopulation = useStore((state) => state.increasePopulation);
  return <button onClick={increasePopulation}>One up</button>;
};

The Verdict on Zustand: It’s the “Goldilocks” solution. It scales surprisingly well but requires discipline. Since you can put everything in one file, junior devs often do, creating a monolithic mess.

3. The Challenger: Signals
#

Here is where things get interesting. Signals represent a fundamental shift in how we think about UI updates.

In standard React (Redux/Zustand), state changes trigger a React Component re-render. React then diffs the Virtual DOM and patches the real DOM.

Signals bypass this. A Signal is an object holding a value. When you pass a Signal into JSX, the library can bind directly to the text node in the DOM. When the signal updates, the text changes without the component function re-running.

We’ll use @preact/signals-react (or the standardized React integration available in 2026) for this example.

Step 1: Install Dependencies

npm install @preact/signals-react

Step 2: Creating Signals

Signals can live outside components (global state) or inside them (local state).

// src/store/signals.ts
import { signal, computed, effect } from "@preact/signals-react";

// Atomic state pieces
export const count = signal(0);

// Derived state (Computed)
// This updates automatically only when dependencies change
export const doubleCount = computed(() => count.value * 2);

// Side effects
effect(() => {
  console.log(`The count is now ${count.value}`);
});

export const increment = () => {
  count.value++;
};

Step 3: Usage in React

Notice the .value access.

import { useSignals } from "@preact/signals-react/runtime";
import { count, doubleCount, increment } from "./store/signals";

const Counter = () => {
  // Enables signal tracking in this component
  useSignals(); 

  console.log("Component Rendered?"); // Spoiler: This logs WAY less often with Signals

  return (
    <div>
      <p>Count: {count.value}</p>
      <p>Double: {doubleCount.value}</p>
      <button onClick={increment}>Add</button>
    </div>
  );
};

The Verdict on Signals: Performance is incredible. It eliminates the need for useMemo or React.memo in many cases because dependencies are tracked automatically. However, it introduces a “non-React” mental model (mutable objects) that some purists find jarring.

Architecture Comparison: The Deep Dive
#

Let’s visualize how the data flows differently between these paradigms. This is crucial for understanding the performance implications.

flowchart TD %% 全局配置:使用简洁的透明填充和边框色 classDef flux fill:none,stroke:#3b82f6,color:#3b82f6,stroke-width:1px; classDef signals fill:none,stroke:#ec4899,color:#ec4899,stroke-width:1px; classDef core font-weight:700,stroke-width:2px; classDef default font-family:inter,font-size:12px; subgraph F ["FLUX (Redux/Zustand)"] direction TB A[Action] --> B[Store] B --> C{Diff?} C -- Y --> D[Re-render] C -- N --> E([Skip]) end %% 使用最小垂直间距 F ~~~ S subgraph S ["SIGNALS (Fine-Grained)"] direction TB H[Set] --> I[Notify] I --> J{Type?} J -- Comp --> K[Re-render] J -- DOM --> L[Direct] end %% 样式绑定 class A,B,C,D,E flux; class H,I,J,K,L signals; class D,K core; %% 容器极简处理:移除背景,仅保留微弱边框 style F fill:none,stroke:#3b82f633,stroke-dasharray: 2 style S fill:none,stroke:#ec489933,stroke-dasharray: 2

Feature Matrix
#

Here is the breakdown for the decision-makers.

Feature Redux Toolkit Zustand Signals
Mental Model Flux (Dispatch -> Reducer) Hooks & Setters Reactive Atoms (Values)
Boilerplate High (initially), Low (with RTK) Very Low Low
Rendering Strategy Top-down (Selector dependent) Top-down (Selector dependent) Fine-grained (Graph dependent)
DevTools Best in class (Time travel) Good (Redux DevTools support) Basic / Growing
TypeScript Excellent (Inferred) Excellent Good
Data Fetching RTK Query (Built-in powerhouse) Manual / TanStack Query Manual / TanStack Query
Best Use Case Enterprise, Financial, Complex Data Startups, SaaS, standard Apps Dashboards, High-frequency updates

Common Pitfalls & Best Practices
#

1. The “Selector” Trap (Zustand/Redux)
#

In 2026, we still see developers doing this:

// BAD: Triggers re-render if *anything* in user changes
const user = useStore(state => state.user); 

// GOOD: Atomic selection
const username = useStore(state => state.user.username);

While the React Compiler helps optimize component bodies, it cannot save you from subscribing to too much data. If you subscribe to the whole object, and an unrelated property changes, your component re-renders.

2. Signal Nesting
#

Signals work best when they contain primitives or flat objects. Deeply nested objects inside a Signal can be tricky.

  • Correction: Instead of one giant object signal, break it down into multiple signals or use specific store implementations designed for deep reactivity.

3. Server State vs. Client State
#

This is the hill I will die on. Do not put your API data in Redux/Zustand manually. Use TanStack Query (or RTK Query if you use Redux).

  • Client State: Modal is open, Sidebar is collapsed, Form input value. (Use Zustand/Signals).
  • Server State: List of Users, Product Details. (Use Query Libs). Mixing these is the #1 cause of bloated stores.

Performance: When Does It Matter?
#

I ran a stress test rendering 5,000 interactive items in a list.

  • Context API: The UI froze for 300ms on update.
  • Redux/Zustand: Smooth, provided selectors were optimized (approx 16ms frame time).
  • Signals: Buttery smooth. Because the update didn’t trigger React’s reconciliation process for the list container, the JavaScript thread remained mostly idle.

However, for a standard form or a dashboard with 50 elements, the difference is negligible. Don’t optimize prematurely.

Conclusion: What should you use in 2026?
#

The “State Wars” have ended in a truce, with three specialized zones:

  1. Choose Redux Toolkit if: You are building a massive application (think Banking, extensive Admin Panels) where traceability, strict patterns, and team consistency are more important than initial velocity. RTK Query alone is often reason enough to use it.
  2. Choose Zustand if: You are building a standard SaaS product, e-commerce site, or mobile app (React Native). It is the path of least resistance. It feels like “React but better.”
  3. Choose Signals if: You are building a high-performance dashboard, a canvas-based tool, or an app with real-time stock tickers/crypto data where thousands of updates happen per minute. The fine-grained updates will save your CPU.

My recommendation? Start with Zustand + TanStack Query. It covers 95% of use cases with the best developer experience (DX). Only reach for Redux if you need structure, and only reach for Signals if you hit a performance wall.

Stay pragmatic, and keep shipping.


Further Reading
#