2026

February 25, 2026

React State Management in 2026: A Data-Driven Comparison

Zustand, Jotai, Nanostores, Redux Toolkit, Valtio, XState, and more. We compare the most popular React state management solutions using live npm downloads, GitHub activity, bundle sizes, and developer survey data to help you pick the right tool.

S
Sascha Becker
Author

22 min read

React State Management in 2026: A Data-Driven Comparison

The state management landscape for React has shifted dramatically over the past two years. Redux is no longer the default. Recoil is archived. Signals are still stuck in committee. And a new generation of tiny, focused libraries has taken over.

We currently use Zustand and are evaluating alternatives, including Nanostores, native React solutions, and other emerging packages. Instead of relying on vibes, we pulled live data from npm, GitHub, and developer surveys to make an informed decision.

The Numbers: npm Weekly Downloads

All numbers pulled from the npm registry API on February 25, 2026.

npm Weekly Downloads (Feb 25, 2026)

Market share by weekly downloads. Zustand and Redux together account for over 60% of all state management downloads.

  • Zustand
  • Redux
  • RTK
  • TanStack
  • XState
  • Jotai
  • MobX
  • Nanostores
  • Valtio
  • Others

Key takeaway: Zustand has overtaken Redux as the most-downloaded dedicated state management library. The TanStack ecosystem is a surprising third force at 7.2M combined downloads. Among the modern lightweight alternatives, Jotai leads at 2.9M, with Nanostores growing fast but still an order of magnitude smaller.

12-Month Trend: The Full Picture

Weekly snapshots only tell part of the story. The chart below shows monthly aggregated downloads from the npm API over the last 12 months. The growth trajectories reveal where the momentum is.

npm Monthly Downloads (Mar 2025 – Feb 2026)

Data from the npm registry API. Feb 2026 is partial (1st–25th).

  • Zustand
  • Redux Toolkit
  • XState
  • MobX
  • Jotai
  • Valtio
  • Nanostores

Zustand's lead is accelerating, not plateauing. It nearly tripled from 26.7M to 72.9M monthly downloads. Redux Toolkit grew too, but at a slower pace, doubling from 18.3M to 37.1M.

The mid-tier libraries (XState, MobX, Jotai) are clustered between 8-12M monthly downloads, all showing steady but unspectacular growth of 30-70%.

But zoom into the smaller libraries and the real surprise emerges:

The Challengers: Jotai vs Nanostores vs Valtio

Nanostores grew 6.6x in 12 months, the fastest growth of any library in this comparison.

  • Jotai
  • Nanostores
  • Valtio

Nanostores is the breakout story. It went from 737K to 4.9M monthly downloads, a 6.6x increase in 12 months. That's faster proportional growth than any other library in this comparison. Much of this is driven by the Astro ecosystem adopting it as the default state solution, but the trajectory is undeniable.

Jotai crossed the line from "promising" to "established" during this period, stabilizing around 9-10M monthly. Valtio remains steady but hasn't found the same growth gear.

GitHub Health Check

A library's GitHub activity tells you whether it will still exist next year. Data pulled from the GitHub API on February 25, 2026.

LibraryStarsOpen IssuesArchived?
Zustand57.1K4No
XState29.3K165No
MobX28.2K86No
Jotai21.0K3No
Recoil19.5K322Yes
Redux Toolkit11.2K260No
Valtio10.1K2No
Nanostores7.1K26No
Effector4.8K151No
Preact Signals4.4K45No
Legend State4.1K210No
TanStack Store79015No

What stands out:

  • Zustand and Jotai have remarkably few open issues (4 and 3) despite massive adoption, a sign of well-maintained, stable codebases.
  • Valtio also shows excellent maintenance with only 2 open issues.
  • Legend State has 210 open issues for only 4K stars, which is concerning.
  • TanStack Store has low stars because it's new and mostly used internally by TanStack Router/Query, but its download numbers tell a different story.

Last Published to npm

How recently a library shipped tells you if it's actively developed or in maintenance mode.

LibraryVersionLast Published
@xstate/store3.16.0Feb 25, 2026
Jotai2.18.0Feb 19, 2026
@legendapp/state2.1.15Feb 19, 2026
@tanstack/react-store0.9.1Feb 17, 2026
@preact/signals-react3.9.0Feb 13, 2026
XState5.28.0Feb 12, 2026
Zustand5.0.11Feb 1, 2026
Effector23.4.4Jan 13, 2026
Valtio2.3.0Jan 1, 2026
Redux Toolkit2.11.2Dec 14, 2025
Nanostores1.1.0Nov 19, 2025
@nanostores/react1.0.0Nov 16, 2025
MobX6.15.0Sep 26, 2025
Recoil0.7.7Feb 12, 2024

Every library except Recoil and MobX has been published within the last 3 months. Nanostores reaching 1.0.0 and 1.1.0 suggests it has stabilized its API.

Bundle Size

Bundle size matters, especially for client-side state that ships to every user. Approximate sizes based on Bundlephobia data and package analysis:

Bundle Size Comparison (min + gzip)

Kilobytes shipped to the client. Nanostores at 0.3 KB is 40x smaller than Zustand.

Developer Sentiment

The State of React 2025 survey (published February 2026) gave us hard usage numbers across three years:

  • Zustand crossed the 50% usage mark (28% → 41% → 50% from 2023 to 2025), nearly doubling in two years.
  • Redux (plain) is still the most widely used at 75.5%, but it's declining from 80.5% in 2023. Redux Toolkit has flatlined at ~54%.
  • Jotai grew steadily from 13% to 19% — still niche, but accelerating.
  • 34% of respondents don't use any state management library at all — useState and useContext are enough.
  • The top pain points across all libraries: excessive complexity (20%) and boilerplate (15%) — both areas where Zustand and Jotai excel.
  • The trend is unmistakable: developers are moving toward simpler, less opinionated tools.

Real-World Comparison: Same Feature, Five Ways

Numbers are great, but what does each library actually feel like in a real codebase? Let's find out.

The scenario: A notifications panel. A <NotificationBell> shows the unread count, a <NotificationPanel> shows the list with filtering, and any component can mark items as read. Server data comes from TanStack Query with generated hooks (via OpenAPI codegen). Each library wraps everything — server data, client UI state, derived values, and mutations — behind a single useNotifications() hook. The component just consumes it.

All examples are TypeScript. Server data comes from TanStack Query with codegen-generated options (the modern pattern — no wrapper hooks, just query option factories spread into useQuery/useMutation):

ts
// Generated by your OpenAPI/GraphQL codegen (e.g. @hey-api/openapi-ts, orval, graphql-codegen)
const { data: notifications = [] } = useQuery({ ...getNotificationsOptions() });
const { mutate: markAsRead } = useMutation({ ...markNotificationAsReadOptions() });
React Context (baseline)
useNotifications.tsx
type Filter = "all" | "unread";
const NotificationCtx = createContext<ReturnType<typeof useNotificationsInner> | null>(null);
function useNotificationsInner() {
const { data: notifications = [] } = useQuery({ ...getNotificationsOptions() });
const { mutate: markAsRead } = useMutation({ ...markNotificationAsReadOptions() });
const [isOpen, setIsOpen] = useState(false);
const [filter, setFilter] = useState<Filter>("all");
const filtered = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
const unreadCount = notifications.filter((n) => !n.read).length;
return {
notifications: filtered,
unreadCount,
isOpen,
toggle: () => setIsOpen((o) => !o),
filter,
setFilter,
markAsRead,
};
}
export function NotificationProvider({ children }: { children: React.ReactNode }) {
const value = useNotificationsInner();
return <NotificationCtx value={value}>{children}</NotificationCtx>;
}
export function useNotifications() {
const ctx = useContext(NotificationCtx);
if (!ctx) throw new Error("Missing NotificationProvider");
return ctx;
}
NotificationBell.tsx
function NotificationBell() {
const { unreadCount, toggle } = useNotifications();
return (
<button onClick={toggle}>
Notifications {unreadCount > 0 && `(${unreadCount})`}
</button>
);
}

The component is clean — but the cost is ~30 lines of Provider boilerplate. The Provider must wrap your tree, and every consumer re-renders when anything changes: toggling isOpen re-renders the bell, the filter buttons, the list, everything. You'd need to split into multiple contexts to fix that, which multiplies the boilerplate.

Zustand
useNotifications.ts
type Filter = "all" | "unread";
const useNotificationUI = create<{
isOpen: boolean;
filter: Filter;
toggle: () => void;
setFilter: (filter: Filter) => void;
}>()((set) => ({
isOpen: false,
filter: "all",
toggle: () => set((s) => ({ isOpen: !s.isOpen })),
setFilter: (filter) => set({ filter }),
}));
export function useNotifications() {
const { data: notifications = [] } = useQuery({ ...getNotificationsOptions() });
const { mutate: markAsRead } = useMutation({ ...markNotificationAsReadOptions() });
const { isOpen, filter, toggle, setFilter } = useNotificationUI();
const filtered = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
const unreadCount = notifications.filter((n) => !n.read).length;
return { notifications: filtered, unreadCount, isOpen, toggle, filter, setFilter, markAsRead };
}
NotificationBell.tsx
function NotificationBell() {
const { unreadCount, toggle } = useNotifications();
return (
<button onClick={toggle}>
Notifications {unreadCount > 0 && `(${unreadCount})`}
</button>
);
}

No Provider. The store holds the UI state, the hook composes it with TanStack Query and exposes a single clean interface. Types are fully inferred. For more granular re-renders, components can also select directly from the store: useNotificationUI((s) => s.isOpen).

Jotai
useNotifications.ts
const isOpenAtom = atom(false);
const filterAtom = atom<"all" | "unread">("all");
export function useNotifications() {
const { data: notifications = [] } = useQuery({ ...getNotificationsOptions() });
const { mutate: markAsRead } = useMutation({ ...markNotificationAsReadOptions() });
const [isOpen, setIsOpen] = useAtom(isOpenAtom);
const [filter, setFilter] = useAtom(filterAtom);
const filtered = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
const unreadCount = notifications.filter((n) => !n.read).length;
return {
notifications: filtered,
unreadCount,
isOpen,
toggle: () => setIsOpen((o) => !o),
filter,
setFilter,
markAsRead,
};
}
NotificationBell.tsx
function NotificationBell() {
const { unreadCount, toggle } = useNotifications();
return (
<button onClick={toggle}>
Notifications {unreadCount > 0 && `(${unreadCount})`}
</button>
);
}

Atoms are the thinnest store definition possible — two lines. The composing hook looks nearly identical to the Zustand version. Where Jotai shines is derived state: if you needed a computed atom that depends on both filterAtom and query data, atom((get) => ...) handles that elegantly. The trade-off: atoms are loose pieces, not a single coherent store, which needs discipline in larger apps.

Nanostores
notification-store.ts
import { atom } from "nanostores";
export const $isOpen = atom(false); // $ prefix is a Nanostores convention, not required
export const $filter = atom<"all" | "unread">("all");
export const toggle = () => $isOpen.set(!$isOpen.get());
export const setFilter = (f: "all" | "unread") => $filter.set(f);
useNotifications.ts
import { useStore } from "@nanostores/react";
export function useNotifications() {
const { data: notifications = [] } = useQuery({ ...getNotificationsOptions() });
const { mutate: markAsRead } = useMutation({ ...markNotificationAsReadOptions() });
const isOpen = useStore($isOpen);
const filter = useStore($filter);
const filtered = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
const unreadCount = notifications.filter((n) => !n.read).length;
return { notifications: filtered, unreadCount, isOpen, toggle, filter, setFilter, markAsRead };
}
NotificationBell.tsx
function NotificationBell() {
const { unreadCount, toggle } = useNotifications();
return (
<button onClick={toggle}>
Notifications {unreadCount > 0 && `(${unreadCount})`}
</button>
);
}

State and actions live in a plain module — no hooks, no React API until the composing hook calls useStore. This is why Nanostores works identically across React, Vue, Svelte, and Astro islands. The $ prefix convention is borrowed from Svelte stores.

Valtio
notification-store.ts
import { proxy } from "valtio";
export const notificationUI = proxy({
isOpen: false,
filter: "all" as "all" | "unread",
});
export const toggle = () => {
notificationUI.isOpen = !notificationUI.isOpen;
};
export const setFilter = (f: "all" | "unread") => {
notificationUI.filter = f;
};
useNotifications.ts
import { useSnapshot } from "valtio";
export function useNotifications() {
const { data: notifications = [] } = useQuery({ ...getNotificationsOptions() });
const { mutate: markAsRead } = useMutation({ ...markNotificationAsReadOptions() });
const { isOpen, filter } = useSnapshot(notificationUI);
const filtered = filter === "unread" ? notifications.filter((n) => !n.read) : notifications;
const unreadCount = notifications.filter((n) => !n.read).length;
return { notifications: filtered, unreadCount, isOpen, toggle, filter, setFilter, markAsRead };
}
NotificationBell.tsx
function NotificationBell() {
const { unreadCount, toggle } = useNotifications();
return (
<button onClick={toggle}>
Notifications {unreadCount > 0 && `(${unreadCount})`}
</button>
);
}

The store uses plain mutations — notificationUI.isOpen = true — while useSnapshot gives you an immutable view for rendering. Valtio tracks which properties each component accesses and only re-renders when those change. The trade-off: proxy behavior can surprise (equality checks, frozen objects, prototype chains).

Head-to-head

Every component looks identical — const { unreadCount, toggle } = useNotifications(). The difference is entirely in how the hook is built.

ContextZustandJotaiNanostoresValtio
Store definitionN/A (inline)~10 lines~2 lines~6 lines~10 lines
Composing hookBuilt into Provider~10 lines~12 lines~10 lines~10 lines
Provider neededYesNoNo*NoNo
Type inferenceManualAutomaticAutomaticExplicit on atomsas assertion
Re-rendersAll consumersSelected slicesPer-atomPer-atomAccessed properties
Works outside ReactNoYesNoYesYes

*Jotai needs a Provider only for SSR or test isolation

The conclusion: every modern library lets you build the same clean useNotifications() hook without a Provider. TanStack Query handles server data, the state library handles UI state, and the composing hook merges them into one interface. The choice comes down to how you prefer to define state — one store (Zustand), independent atoms (Jotai/Nanostores), or mutable objects (Valtio).

The Contenders: Deep Dive

Zustand — The New Default

Zustand has become the "just use this" recommendation in the React community, and the numbers back it up.

tsx
import { create } from "zustand";
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Counter() {
const count = useStore((state) => state.count);
const increment = useStore((state) => state.increment);
return <button onClick={increment}>{count}</button>;
}

Strengths:

  • Minimal API, almost no boilerplate
  • No Provider wrapper needed
  • Works outside React (vanilla JS, middleware)
  • Excellent TypeScript support
  • Selector-based re-render optimization
  • Middleware ecosystem (persist, devtools, immer)
  • Active development by the pmndrs collective (Dai Shi)

Weaknesses:

  • Selector patterns can get verbose for complex state
  • No built-in computed/derived state (need middleware)
  • Single store pattern can lead to large store files

Best for: Most React applications. The safe, boring, correct choice.

Jotai — Atomic Precision

Jotai takes a bottom-up, atomic approach inspired by Recoil but without the baggage. Each piece of state is an independent atom.

tsx
import { atom, useAtom } from "jotai";
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
function Display() {
const [doubled] = useAtom(doubledAtom);
return <span>{doubled}</span>;
}

Strengths:

  • Derived/computed state is a first-class concept
  • Components only re-render when their specific atoms change
  • Excellent for complex interdependent state
  • Tiny bundle, great TypeScript support
  • Atom-level code splitting
  • Integration with async data (suspense-compatible atoms)

Weaknesses:

  • Mental model shift from centralized stores
  • Atom proliferation in large apps needs discipline
  • Debugging can be harder (state is distributed)

Best for: Applications with complex, interdependent state. Form builders, dashboards with lots of computed values.

Nanostores — Radical Minimalism

Nanostores is the smallest state management library by a wide margin. Created by Andrey Sitnik (the author of PostCSS and Autoprefixer), it takes a framework-agnostic, atomic approach.

tsx
import { atom, computed } from "nanostores";
import { useStore } from "@nanostores/react";
const $count = atom(0);
const $doubled = computed($count, (count) => count * 2);
function Counter() {
const count = useStore($count);
return <button onClick={() => $count.set(count + 1)}>{count}</button>;
}

Strengths:

  • 286 bytes. That's not a typo.
  • Framework-agnostic (React, Vue, Svelte, Solid, Angular, Lit)
  • Zero dependencies
  • Simple, predictable API
  • Great for multi-framework projects (Astro)
  • Stable 1.0 API

Weaknesses:

  • Smaller community and ecosystem
  • Fewer middleware/plugins than Zustand
  • Less React-specific ergonomics (imperative .set() vs hooks)
  • Documentation is thinner than major competitors
  • @nanostores/react adapter adds overhead to the base size

Best for: Bundle-critical applications, multi-framework projects, Astro sites, teams who value radical simplicity.

Valtio — Proxy Magic

Valtio uses JavaScript Proxies to make state mutations feel natural while keeping React happy with immutable snapshots under the hood.

tsx
import { proxy, useSnapshot } from "valtio";
const state = proxy({ count: 0 });
function Counter() {
const snap = useSnapshot(state);
return <button onClick={() => state.count++}>{snap.count}</button>;
}

Strengths:

  • Most intuitive API (just mutate objects)
  • Automatic tracking of accessed properties
  • Works well with nested state
  • Low boilerplate

Weaknesses:

  • Proxy behavior can surprise (equality, frozen objects)
  • Harder to debug (mutations happen outside React's model)
  • Smaller ecosystem than Zustand

Best for: Teams coming from MobX or vanilla JS who find immutable patterns annoying.

Redux Toolkit — The Enterprise Standard

RTK modernized Redux with opinionated defaults, but it's still Redux under the hood.

Strengths:

  • Battle-tested at scale (Meta, Amazon, etc.)
  • RTK Query for data fetching
  • Time-travel debugging with Redux DevTools
  • Massive ecosystem and hiring pool
  • Structured patterns benefit large teams

Weaknesses:

  • Still heavy (~12 KB gzipped)
  • Boilerplate has been reduced but not eliminated
  • Slices, selectors, dispatching: steeper learning curve
  • High negative sentiment across State of React surveys

Best for: Large enterprise teams with existing Redux infrastructure. New projects should think twice.

XState & @xstate/store — State Machines

XState brings formal state machines to JavaScript. The new @xstate/store extracts the simple event-driven pattern without the full state machine overhead.

Best for: Complex workflows (wizards, payment flows, multi-step processes). The full XState is overkill for simple UI state.

TanStack Store — The Quiet Giant

TanStack Store is a surprising entry on the charts. At 7.2M weekly downloads it looks like a major player, but context matters: the vast majority of those downloads come from being an internal dependency of TanStack Router, TanStack Table, and other TanStack tools — not from standalone adoption. Its 790 GitHub stars tell a different story than the download numbers.

tsx
import { Store } from "@tanstack/store";
import { useStore } from "@tanstack/react-store";
const store = new Store({ count: 0 });
function Counter() {
const count = useStore(store, (s) => s.count);
return (
<button onClick={() => store.setState((s) => ({ ...s, count: s.count + 1 }))}>
{count}
</button>
);
}

Strengths:

  • Framework-agnostic (React, Vue, Solid, Angular, Svelte)
  • TypeScript-first with excellent type inference
  • Backed by the TanStack ecosystem (Tanner Linsley)
  • Selector-based re-render optimization
  • No Provider needed

Weaknesses:

  • Most downloads are transitive, not intentional adoption
  • Tiny community (790 stars, limited discussions)
  • Sparse documentation — mostly API reference, few guides
  • No middleware ecosystem (no persist, no devtools)
  • Immature compared to Zustand and Jotai

Best for: Projects already deep in the TanStack ecosystem (Router, Table, Query) that want a consistent API across their stack. Not yet compelling enough as a standalone choice.

The Others
  • MobX (28K stars, 2.9M downloads): Mature and stable, but hasn't published since September 2025. Observable-based reactivity. If you're already using it, no rush to migrate. If you're not, there's no reason to start.
  • @legendapp/state (4K stars, 45K downloads): Claims to be the fastest with fine-grained reactivity and built-in persistence/sync. Interesting for React Native. But 210 open issues on 4K stars is a red flag.
  • @preact/signals-react (4.4K stars, 246K downloads): Bypasses React's rendering model entirely for maximum performance. Fragile across React versions and incompatible with Server Components. The React team has explicitly said signals go against React's model.
  • Effector (4.8K stars, 62K downloads): Event-driven reactive state with a strong following in the Russian-speaking community. Excellent for complex business logic but niche.

The Native React Story

Before reaching for any library, consider whether React's built-in primitives are enough.

React 19 brought:
  • use hook for consuming promises and context
  • useActionState for form state with server actions
  • useOptimistic for optimistic UI updates
  • Improved Context with the <Context value={}> shorthand
React Compiler (v1.0, October 2025):

The React Compiler shipped as stable and provides automatic memoization at build time. This weakens one of the main arguments for external state management: performance optimization. If React itself handles memoization, the re-render optimization argument for signals and proxies is significantly reduced.

Meta's own data shows up to 12% faster initial loads and 2.5x faster interactions with the compiler enabled.

When built-in state is enough:

The developerway.com analysis breaks it down well:

  1. Server state (~80% of what used to be Redux): Use TanStack Query or SWR
  2. URL state (~10%): Use nuqs or your router
  3. Local component state: useState / useReducer
  4. Shared component state: Prop drilling or Context for 1-2 concerns

You only need an external state management library when you have 3+ shared client-side concerns that Context can't handle without "Provider hell."

The TC39 Signals Proposal

The TC39 Signals proposal remains at Stage 1 as of February 2026. The committee is taking a conservative approach, requiring real-world integration into multiple frameworks before advancing. Angular, Solid, Vue, and Preact support it. The React team does not, stating that signals conflict with React's "UI as a function of state" model.

Don't bet your architecture on TC39 Signals. If they ever land in the spec, it will be years from now, and React may never adopt them directly.

Decision Matrix

CriteriaZustandJotaiNanostoresValtioRTKTanStack Store
Bundle size1.2 KB3.5 KB0.3 KB3.2 KB12 KB1.4 KB
Learning curveLowMediumLowLowHighLow
TypeScript DXExcellentExcellentGoodGoodGoodExcellent
Computed stateVia middlewareBuilt-inBuilt-inAutomaticVia selectorsVia selectors
DevToolsRedux DevToolsJotai DevToolsNoneValtio DevToolsRedux DevToolsNone
SSR/RSC compatGoodGoodGoodGoodGoodGood
Framework agnosticPartialNoYesPartialNoYes
Community sizeVery largeLargeMediumMediumVery largeSmall
MaintenanceExcellentExcellentGoodExcellentGoodActive
Weekly downloads22.6M2.9M1.5M1.2M11.4M7.2M*

*Most TanStack Store downloads are transitive (from TanStack Router, Table, etc.), not standalone adoption.

Our Recommendation

There's no universal answer, but here's how we think about it:

For most React projects: Zustand is the safe, pragmatic default. It's tiny, well-maintained, has the largest modern community, and the simplest mental model. The 4 open issues on 57K stars speaks volumes.

If you have complex derived state: Jotai is the better choice. Its atomic model with first-class computed atoms handles complex interdependencies more elegantly than Zustand's selectors.

If bundle size is critical: Nanostores wins at 286 bytes. It's also the right choice if you're in a multi-framework project (Astro, mixing React with other frameworks). But you're trading ecosystem size and React-specific ergonomics for minimal footprint.

If you need proxy-based mutations: Valtio offers the most natural API for developers who find immutable patterns verbose. Same author as Zustand (Dai Shi), so quality is there.

If you're already on Redux: Redux Toolkit is fine. No need to migrate for the sake of it. But for new projects, the overhead is hard to justify.

If you're deep in the TanStack ecosystem: TanStack Store gives you a consistent API alongside Router and Table. But as a standalone state management choice, it doesn't yet offer enough over Zustand or Jotai to justify the smaller community and missing ecosystem.

The TanStack Query + Zustand stack (or Jotai) is the emerging default for 2026: server state in TanStack Query, client state in a lightweight store.

Sources

All data in this article was collected on February 25, 2026 from:


S
Written by
Sascha Becker
More articles