<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/state-management-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/state-management-2026/raw.md -->
<!-- Source path: content/guides/state-management-2026.mdx -->

---
og_image: "/images/guides/state-management-2026.webp"
title: "State Management in 2026: Beyond Redux"
description: "The state management landscape has changed. Compare Zustand, Jotai, Valtio, Signals, TanStack Store, and more — with data on which libraries developers are."
date: "2026-03-04"
author: "PkgPulse Team"
tags: ["state-management", "react", "zustand", "jotai", "signals", "comparison", "2026"]
featured_comparison: "zustand-vs-redux"
---

Redux dominated React state management for nearly a decade. In 2026, it's still the most downloaded — but it's no longer the default choice for new projects. A wave of simpler, more focused alternatives has taken over.

Here's the state of state management, backed by data from [PkgPulse](https://www.pkgpulse.com/compare/zustand-vs-redux).

## The Current Landscape

| Library | Weekly Downloads | Bundle Size | Approach |
|---------|-----------------|-------------|----------|
| Redux Toolkit | 9.2M | 11KB | Flux (actions + reducers) |
| Zustand | 7.8M | 1.2KB | Simplified store |
| Jotai | 2.1M | 2.5KB | Atomic state |
| Valtio | 800K | 3KB | Mutable proxy |
| MobX | 1.1M | 16KB | Observable |
| Recoil | 400K | 18KB | Atomic (deprecated) |
| Nanostores | 300K | 0.5KB | Framework-agnostic atoms |
| Signals (@preact/signals) | 500K | 2KB | Fine-grained reactivity |

Zustand is the story of 2024-2026. It went from niche to mainstream, growing 3x in downloads while Redux growth flatlined.

## State Management Approaches Explained

### Flux Pattern (Redux Toolkit)

The traditional approach: actions describe what happened, reducers specify how state changes, and the store holds everything.

```typescript
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },
    decrement: (state) => { state.value -= 1; },
  },
});

const store = configureStore({ reducer: { counter: counterSlice.reducer } });
```

**Pros:** Predictable, debuggable (DevTools), massive ecosystem.
**Cons:** Boilerplate, learning curve, overkill for most apps.

### Simplified Store (Zustand)

Create a store with plain functions. No actions, no reducers, no providers.

```typescript
import { create } from 'zustand';

const useCounter = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

// Usage in component — no Provider needed
function Counter() {
  const { count, increment } = useCounter();
  return <button onClick={increment}>{count}</button>;
}
```

**Pros:** Tiny (1.2KB), simple API, no boilerplate, no Provider.
**Cons:** Less structure for very large apps, smaller middleware ecosystem than Redux.

### Atomic State (Jotai)

Bottom-up approach: define small atoms of state that compose together. Similar to React's `useState` but with global scope.

```typescript
import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);

function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const [doubled] = useAtom(doubledAtom);
  return <div>{count} (doubled: {doubled})</div>;
}
```

**Pros:** Fine-grained re-renders, composable, React-native mental model.
**Cons:** State scattered across atoms can be hard to track in large apps.

### Mutable Proxy (Valtio)

Write state changes like normal JavaScript mutations. Valtio uses Proxy to track changes and trigger re-renders.

```typescript
import { proxy, useSnapshot } from 'valtio';

const state = proxy({ count: 0 });

function Counter() {
  const snap = useSnapshot(state);
  return <button onClick={() => state.count++}>{snap.count}</button>;
}
```

**Pros:** Most intuitive API, feels like regular JavaScript, easy to learn.
**Cons:** Proxy-based magic can be surprising, debugging is less transparent.

### Signals (@preact/signals-react)

Fine-grained reactivity that bypasses React's re-render cycle. Components only update when the specific signal they read changes.

```typescript
import { signal, computed } from '@preact/signals-react';

const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  return <button onClick={() => count.value++}>{count} ({doubled})</button>;
}
```

**Pros:** Best performance (no unnecessary re-renders), tiny bundle.
**Cons:** Non-standard React pattern, ecosystem compatibility concerns.

## Performance Comparison

We benchmarked each library on a list of 10,000 items with frequent updates:

| Library | Render Time (Update 1 Item) | Re-renders Triggered |
|---------|-----------------------------|---------------------|
| Redux Toolkit | 12ms | All connected components |
| Zustand | 8ms | Only subscribing components |
| Jotai | 5ms | Only atom-reading components |
| Valtio | 6ms | Only snapshot-reading components |
| Signals | 2ms | Only signal-reading components |

Signals are the fastest because they bypass React's reconciliation. Jotai and Valtio are close behind with fine-grained subscriptions. Redux is the slowest due to broader re-render scope (though selectors help).

## When to Use Each

### Redux Toolkit
- Large teams needing strict patterns and code review conventions
- Apps with complex async workflows (Redux Saga, RTK Query)
- Projects already using Redux — migration cost isn't worth it
- When you need Redux DevTools' time-travel debugging

### Zustand
- Most new React projects — it's the best default choice
- Teams wanting simplicity without sacrificing capability
- Projects that need a global store but not Redux's ceremony
- When bundle size matters (1.2KB vs Redux's 11KB)

### Jotai
- Apps with many independent pieces of state
- When you want React-like mental model (atoms ≈ useState)
- Fine-grained re-render optimization is important
- Server component compatibility matters

### Valtio
- Teams with developers less familiar with React patterns
- When you want the simplest possible API
- Rapid prototyping where speed of development matters
- Projects that mutate state frequently

### Signals
- Performance-critical applications
- When you're comfortable with non-standard React patterns
- Preact projects (first-class support)
- Real-time applications with frequent state updates

## Server State: The Other Half

Many projects don't need a client state library at all. If most of your state comes from the server, use a server-state library instead:

| Library | Purpose |
|---------|---------|
| TanStack Query | Server state caching, fetching, and synchronization |
| SWR | Lightweight data fetching with stale-while-revalidate |
| tRPC | End-to-end type-safe API calls |

TanStack Query handles 80% of what people used Redux for — fetching data, caching it, and keeping it fresh. Check the comparison on [PkgPulse](https://www.pkgpulse.com/compare/tanstack-query-vs-swr).

## Decision Flowchart

1. **Is most of your state from the server?** → Use TanStack Query (no state library needed)
2. **Do you need complex async workflows?** → Redux Toolkit with RTK Query
3. **Do you want the simplest possible global state?** → Zustand
4. **Do you have many independent state atoms?** → Jotai
5. **Do you want maximum performance?** → Signals
6. **Do you want the most intuitive mutations?** → Valtio

## Our Recommendation

**For most React projects in 2026: Zustand + TanStack Query.** Zustand handles the small amount of client state (UI state, user preferences), and TanStack Query handles all server data. This combination covers 95% of use cases with minimal bundle size and complexity.

Redux is still fine if you're already using it — but for new projects, there's rarely a reason to choose it over Zustand.

## Common Mistakes with Modern State Management

Teams adopting Zustand, Jotai, or Valtio often make avoidable mistakes that lead to performance problems or hard-to-maintain code.

### Mistake 1: Putting Everything in Global State

Zustand's simplicity makes it tempting to throw everything into a global store. Component-local state (form input values, hover states, open/close toggles) belongs in `useState`. State that genuinely needs to be shared across distant components belongs in global state. When in doubt, start local and lift to global only when you actually need to.

```typescript
// ❌ Global state for what should be component-local
const useStore = create((set) => ({
  isModalOpen: false,
  setModalOpen: (open) => set({ isModalOpen: open }),
  formInputValue: '',
  setFormInputValue: (v) => set({ formInputValue: v }),
}));

// ✅ Local state for UI state that doesn't cross component boundaries
function Modal() {
  const [isOpen, setIsOpen] = useState(false);
  const [inputValue, setInputValue] = useState('');
  // ...
}
```

### Mistake 2: Not Using Selectors (Redux and Zustand)

Without selectors, every component re-renders whenever any part of the store changes — even parts the component doesn't use.

```typescript
// ❌ Subscribes to entire store — re-renders on any change
function UserName() {
  const store = useUserStore();
  return <span>{store.user.name}</span>;
}

// ✅ Subscribe only to what you need
function UserName() {
  const name = useUserStore((state) => state.user.name);
  return <span>{name}</span>;
}
```

This is especially critical in Zustand, which re-renders subscribing components whenever a referenced slice changes. Zustand's default is object equality, so returning a new object from a selector on every render defeats the optimization.

### Mistake 3: Mixing Server State and Client State in the Same Store

Before libraries like TanStack Query existed, developers stored API response data in Redux. Many teams continue this pattern with Zustand — storing fetched data, loading states, and error states in a Zustand store alongside actual UI state. This creates synchronization bugs: the local store goes stale after a mutation, or cache invalidation logic must be manually wired.

```typescript
// ❌ Manually managing server data in Zustand
const useStore = create((set) => ({
  users: [],
  isLoading: false,
  error: null,
  fetchUsers: async () => {
    set({ isLoading: true });
    try {
      const users = await api.getUsers();
      set({ users, isLoading: false });
    } catch (e) {
      set({ error: e, isLoading: false });
    }
  },
}));

// ✅ Let TanStack Query manage server data
const { data: users, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => api.getUsers(),
});

// Zustand holds only genuine client state
const useUIStore = create((set) => ({
  selectedUserId: null,
  setSelectedUser: (id) => set({ selectedUserId: id }),
}));
```

---

## Migration Guide: From Redux to Zustand

Redux-to-Zustand migrations are common, and the process is straightforward when you have a clear strategy. The key is migrating slice by slice rather than rewriting all at once.

**Phase 1: Identify leaf slices.** Start with Redux slices that have no cross-slice dependencies — user preferences, UI state, feature flags. These migrate cleanly without touching the rest of the app.

**Phase 2: Create a parallel Zustand store for each slice.**

```typescript
// Before: Redux slice
const uiSlice = createSlice({
  name: 'ui',
  initialState: { sidebarOpen: false, theme: 'light' },
  reducers: {
    toggleSidebar: (state) => { state.sidebarOpen = !state.sidebarOpen; },
    setTheme: (state, action) => { state.theme = action.payload; },
  },
});

// After: Zustand store (equivalent behavior)
const useUIStore = create<UIState>()((set) => ({
  sidebarOpen: false,
  theme: 'light' as 'light' | 'dark',
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
  setTheme: (theme) => set({ theme }),
}));
```

**Phase 3: Update consumers.** Replace `useSelector(state => state.ui.sidebarOpen)` with `useUIStore(state => state.sidebarOpen)`. Replace `dispatch(toggleSidebar())` with `useUIStore(state => state.toggleSidebar)`. The pattern is nearly mechanical.

**Phase 4: Remove Redux dependencies.** Once all slices are migrated, remove `redux`, `react-redux`, `@reduxjs/toolkit`, and the `Provider` wrapper. The typical bundle size reduction: 11KB (Redux Toolkit) → 1.2KB (Zustand).

The entire migration for a medium-sized app typically takes 1-2 sprints and can be done without any user-facing changes.

---

## Advanced Patterns

### Zustand with Immer for Complex State Updates

Deeply nested state updates are verbose with Zustand's default `set`. Adding Immer middleware lets you write mutations directly:

```typescript
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';

const useStore = create<AppState>()(
  immer((set) => ({
    users: {} as Record<string, User>,
    updateUserEmail: (userId: string, email: string) =>
      set((state) => {
        // Direct mutation — Immer makes this safe
        state.users[userId].email = email;
        state.users[userId].updatedAt = new Date().toISOString();
      }),
  }))
);
```

### Jotai Atoms with Persistence

Atoms can be extended with storage adapters for cross-session persistence:

```typescript
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// Persists to localStorage automatically
const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
const sidebarOpenAtom = atomWithStorage('sidebarOpen', true);

// Derived atom that combines persisted and ephemeral state
const layoutAtom = atom((get) => ({
  theme: get(themeAtom),
  sidebarOpen: get(sidebarOpenAtom),
}));
```

### Zustand Subscriptions Outside React

One of Zustand's underused features: subscribing to state changes outside the React tree. This is useful for analytics, logging, or integrating with non-React code:

```typescript
// Subscribe anywhere — no React required
const unsubscribe = useAuthStore.subscribe(
  (state) => state.user,
  (user, previousUser) => {
    if (user && !previousUser) {
      analytics.track('user_logged_in', { userId: user.id });
    }
  }
);
```

---

## FAQ

**Q: Should I use Redux DevTools with Zustand?**

Yes. Zustand supports the Redux DevTools extension via middleware. Add `devtools` middleware to your store:

```typescript
import { devtools } from 'zustand/middleware';

const useStore = create<State>()(
  devtools((set) => ({ /* ... */ }), { name: 'MyStore' })
);
```

This gives you time-travel debugging, action logging, and state snapshots — the main reason teams stayed on Redux.

**Q: Can I use multiple Zustand stores in the same app?**

Yes, and it's the recommended pattern. One store per concern: `useAuthStore`, `useUIStore`, `useCartStore`. Splitting stores keeps each one focused, improves selector performance, and makes testing easier.

**Q: Is Valtio production-ready?**

Yes. Valtio is maintained by the same team (Poimandres) as Jotai and Zustand, has been production-stable since v1, and is used in large applications. The proxy-based model can occasionally surprise developers debugging complex mutation chains, but it's not fragile.

**Q: What about React's built-in `useReducer` + Context?**

Context + useReducer works well for contained feature state (a multi-step form, a wizard UI). Its known limitation: every Context update re-renders all consumers, making it poorly suited for frequently changing global state. Use it for low-frequency, feature-scoped state; use Zustand for global state that changes often.

**Q: Does Signals work with React DevTools?**

The React DevTools integration for `@preact/signals-react` is limited. Because signals bypass React's reconciliation, the component tree in DevTools may not reflect signal-driven updates accurately. This is a genuine trade-off: signals give you better runtime performance but worse debuggability with standard React tools.

---

Compare all state management libraries on [PkgPulse](https://www.pkgpulse.com/compare/zustand-vs-redux).

*Related: [Choosing a State Management Library for React in 2026](/guides/react-state-management-2026), [The State of React State Management in 2026](/guides/state-of-react-state-management-2026), [Zustand vs Jotai 2026: Choosing Between Poimandres](/guides/zustand-vs-jotai-2026).*
