React State Management: pmndrs/zustand vs reduxjs/redux-toolkit
When component props and Context aren't enough for shared React state, the two real choices are Zustand (pmndrs/zustand) for a tiny hook-based store with almost no boilerplate, and Redux Toolkit (reduxjs/redux-toolkit) for a structured, DevTools-backed flow.
React state managers hold data outside a single component and let many components read and update it without prop drilling. pmndrs/zustand exposes one create() hook with no provider; reduxjs/redux centralizes everything in a single store updated by pure reducers; pmndrs/jotai builds state bottom-up from tiny atoms you compose like useState. They differ mainly in how much structure they impose.
pmndrs/zustand is the default choice for most React teams today. You call create() once, get a hook, and read slices with a selector — no boilerplate action types, no provider wrapping your tree, no connect(). Zustand re-renders only the components whose selected slice changed, and its persist, Immer, and Redux DevTools middleware cover the common needs without extra plumbing.
reduxjs/redux-toolkit is the strong runner-up and the right call for large teams that want one enforced update pattern, serializable action logs, and time-travel DevTools; its createSlice and bundled RTK Query cut classic Redux boilerplate sharply. For server data, TanStack/query owns caching, refetching, and request dedupe. For tangled multi-step flows, statelyai/xstate models state as explicit machines.
If you want shared client state with minimal ceremony, pick pmndrs/zustand. If a large team needs strict, auditable, one-way updates with time-travel debugging, pick reduxjs/redux-toolkit. If your hard problem is fetching, caching, and syncing server data, pick TanStack/query — or vercel/swr for a lighter fetch hook. If your logic is a tangle of states and transitions, pick statelyai/xstate.
pmndrs/zustand wins for most React teams: a single create() call returns a hook, there is no provider to wire up, and selector subscriptions keep re-renders surgical. Its persist, Immer, and DevTools middleware cover the rest with almost no boilerplate.
reduxjs/redux-toolkit is the modern, official Redux. createSlice plus built-in Immer removes the old boilerplate, RTK Query handles data fetching, and you keep serializable actions and time-travel DevTools for large, audit-heavy applications.
pmndrs
zustand
- 58,448Stars
- 22Hotness
- 60Reliability
- over 7 yearsAge
pmndrs/zustand is a minimal client-state library built around a single create() call that returns a hook. You write const useStore = create((set) => ({ count: 0, inc: () => set((s) => ({ count: s.count + 1 })) })), then read slices anywhere with useStore((s) => s.count). Zustand needs no context provider, the store lives in module scope, and components re-render only when the selected slice changes.
Zustand's strengths: the API is tiny, so onboarding takes minutes; selector-based subscriptions keep re-renders surgical without memoization gymnastics; and middleware for persistence, Immer, and Redux DevTools ships in the box. Drawbacks: there is no enforced structure, so large teams can sprawl undisciplined stores; it has no built-in async server-cache layer, so you still reach for TanStack/query for data fetching; and the lack of a provider can complicate per-request isolation in SSR.
Pick pmndrs/zustand when you want global or shared client state with almost no ceremony and don't need Redux's audit trail. Choose reduxjs/redux-toolkit instead when you need strict action logging, time-travel debugging, or a large team enforcing one update pattern.
reduxjs
redux-toolkit
- 11,226Stars
- 27Hotness
- 54Reliability
- over 8 yearsAge
reduxjs/redux-toolkit (RTK) is the official, opinionated wrapper over Redux. configureStore sets up the store with sensible defaults, and createSlice generates action creators and reducers together while letting you write 'mutating' code that immerjs/immer converts to immutable updates. RTK Query, bundled in, adds a full data-fetching and caching layer with auto-generated hooks.
RTK's strengths: it cuts classic Redux boilerplate by more than half while keeping serializable actions and time-travel DevTools; createSlice colocates state, reducers, and actions so features stay self-contained; and RTK Query handles caching, invalidation, and request dedupe without a separate library. Drawbacks: the conceptual surface is still larger than pmndrs/zustand or pmndrs/jotai; there's meaningful bundle weight; and the Redux mental model of actions, reducers, and selectors is more than small apps need.
Pick reduxjs/redux-toolkit when a large team wants one enforced, auditable update pattern and strong DevTools. If you want the same shared-state outcome with a fraction of the code and no team mandate, choose pmndrs/zustand instead.
vuejs
pinia
- 14,670Stars
- 31Hotness
- 1Reliability
- over 6 yearsAge
vuejs/pinia is the official state store for Vue, not React. You defineStore('cart', () => { ... }) using the composition API, returning reactive state, getters, and actions, then call useCartStore() inside components. State is mutable and tracked by Vue's reactivity system, with full Vue DevTools and TypeScript support.
Pinia's strengths: the store API is intuitive and type-safe, getters give computed derived state, and DevTools integration with time-travel is excellent. Its drawback in this context is decisive: Pinia targets Vue's reactivity, so it is not a React state manager and won't drop into a React app. Within Vue it's the clear successor to Vuex, but that's a different framework than the rest of this list, so a React-only comparison doesn't apply to it.
Choose vuejs/pinia only if you're building in Vue, where it's the default store. In a React codebase it doesn't apply — reach for pmndrs/zustand or reduxjs/redux-toolkit instead; the closest tiny cross-framework option here is nanostores/nanostores.
nanostores
nanostores
- 7,497Stars
- 36Hotness
- 1Reliability
- almost 6 yearsAge
nanostores/nanostores is a tiny (a few hundred bytes) framework-agnostic state manager built from atoms and maps. You create const counter = atom(0), update with counter.set(...), and bind it to React via useStore(counter) from @nanostores/react. The same stores work in Preact, Vue, Svelte, and vanilla JS, and computed stores derive from others.
nanostores's strengths: the bundle cost is almost nothing and each store is independently tree-shakable, which is ideal for islands and micro-frontends; it's framework-agnostic, so shared logic spans React and other UIs; and the API is minimal. Drawbacks: it's deliberately lightweight, so there's no rich DevTools or middleware ecosystem like Redux has; large apps must impose their own structure; and the community is smaller than zustand's or Redux's.
Choose nanostores/nanostores when bundle size is paramount or you share state across multiple frameworks — Astro islands are a sweet spot. For a React-only app that wants more tooling and examples, pmndrs/zustand is the safer default.
TanStack
query
- 49,954Stars
- 21Hotness
- 89Reliability
- almost 7 yearsAge
TanStack/query (React Query) manages server state, not client state. You call useQuery({ queryKey: ['todos'], queryFn }) and it fetches, caches by key, dedupes concurrent requests, refetches on focus and reconnect, and exposes loading, error, and data. Mutations use useMutation with cache invalidation. It deliberately doesn't store your local UI state — it owns the async data layer.
TanStack/query's strengths: it eliminates the hand-rolled loading, error, and cache boilerplate that bloats Redux apps; background refetching and stale-while-revalidate keep data fresh automatically; and it offers pagination, infinite scroll, and optimistic updates as first-class features. Drawbacks: it's not for synchronous client state, so you still pair it with pmndrs/zustand or context; the caching model has a learning curve; and misconfigured query keys cause subtle staleness bugs.
Pick TanStack/query whenever your data lives on a server and you're tired of writing fetch, cache, and refetch logic. For a lighter, simpler fetch hook choose vercel/swr; for local UI state pair it with pmndrs/zustand — they solve different halves of the problem.
statelyai
xstate
- 29,888Stars
- 20Hotness
- 87Reliability
- almost 11 yearsAge
statelyai/xstate models logic as explicit finite state machines and statecharts. You declare states, events, and transitions in createMachine({ ... }), then run it with an actor or the @xstate/react useMachine hook. State can only move along defined transitions, so impossible states become unrepresentable, and the machine handles guards, side effects, and spawned child actors.
XState's strengths: it makes complex flows — multi-step forms, wizards, async workflows — provably correct by forbidding invalid transitions; the statechart can be visualized and shared with designers via Stately's tools; and its actor model handles concurrency cleanly. Drawbacks: the learning curve is the steepest on this list; the config is verbose for trivial state; and it's overkill for simple toggles or a shared counter where pmndrs/zustand would be a few lines.
Pick statelyai/xstate when your hard problem is genuinely stateful logic with many states and transitions you keep getting wrong. For ordinary global data with no complex flow, choose pmndrs/zustand or pmndrs/jotai instead — xstate's structure would be pure overhead.
pmndrs
jotai
- 21,229Stars
- 23Hotness
- 61Reliability
- almost 6 yearsAge
pmndrs/jotai builds state from the bottom up out of atoms. You declare const countAtom = atom(0) and consume it with const [count, setCount] = useAtom(countAtom), exactly like useState but shared. Derived atoms compute from other atoms — atom((get) => get(countAtom) * 2) — and Jotai tracks the dependency graph, re-rendering only components that read affected atoms.
Jotai's strengths: the atom model eliminates selector boilerplate and the re-render granularity is automatic and fine; it handles async by letting atoms return promises that suspend; and it composes cleanly for derived and computed state. Drawbacks: atoms-everywhere can scatter state definitions across a codebase with no central store to inspect; debugging the dependency graph is less mature than Redux DevTools; and very large derived graphs can get hard to trace.
Choose pmndrs/jotai when you want useState-style ergonomics with shared, derived, and async state and dislike Redux ceremony. If you prefer a single inspectable store object over scattered atoms, pmndrs/zustand fits better; if you want mutable proxy-style writes, pmndrs/valtio.
vercel
swr
- 32,434Stars
- 11Hotness
- 62Reliability
- over 6 yearsAge
vercel/swr is a React hook for data fetching named after the stale-while-revalidate strategy. You call const { data, error, isLoading } = useSWR('/api/user', fetcher) and SWR returns cached data immediately, then revalidates in the background. It dedupes requests, refetches on focus and reconnect, and keys the cache by the request string.
SWR's strengths: the API is extremely small — one hook covers most fetching needs; it's lightweight and from the Next.js team, so it pairs naturally with Vercel and Next apps; and built-in revalidation and dedupe remove a lot of manual cache code. Drawbacks: it does less than TanStack/query — fewer mutation, pagination, and cache-control features out of the box; like Query it's server-state only, not local state; and complex caching scenarios may outgrow it.
Pick vercel/swr when you want the simplest possible fetch-and-cache hook, especially in a Next.js app. If you need richer mutations, infinite queries, and fine cache control, choose TanStack/query; for local client state, pair either with pmndrs/zustand.
TanStack
store
- 864Stars
- 11Hotness
- 1Reliability
- almost 3 yearsAge
TanStack/store is a small, framework-agnostic, type-safe reactive store that underpins other TanStack libraries. You create a new Store(initialState), update via store.setState, derive values with Derived, and subscribe from React through an adapter hook. It's a low-level reactive primitive rather than an opinionated application-state framework.
TanStack/store's strengths: it's tiny, strongly typed, and works across React, Solid, Svelte, and Vue via adapters; reactive derivations are first-class; and it's a solid foundation for building your own state layer. Drawbacks: it's young and far less popular than the rest of this list, so docs and examples are thin; being low-level, it prescribes little structure; and most app teams want something higher-level out of the box.
Choose TanStack/store if you're building inside the TanStack ecosystem or want a minimal typed reactive core to build on. For an app-ready store with a bigger community and middleware, pick pmndrs/zustand; for the atom model, pmndrs/jotai.
react-hookz
web
- 2,209Stars
- 5Hotness
- 1Reliability
- over 5 yearsAge
react-hookz/web is a large collection of well-tested React hooks for browser and SSR, not a dedicated state manager. It ships utilities like useLocalStorageValue, useMediaQuery, useDebouncedState, and useToggle — many of which manage small pieces of component or persisted state. You import the specific hook you need rather than setting up a central store.
react-hookz/web's strengths: the hooks are SSR-safe, TypeScript-first, and tree-shakable, so you only ship what you use; it covers a wide range of common needs that would otherwise be hand-rolled; and it's a modern, maintained successor to older hook grab-bags. Drawbacks: it offers no global or shared store at all, so cross-component app state is out of scope; its breadth means a larger surface to learn; and it overlaps with utilities you may already have.
Reach for react-hookz/web for local, per-component, and persisted-widget state plus browser utilities. For genuine app-wide shared state it isn't the tool — pair it with pmndrs/zustand or pmndrs/jotai, which own the global-store role it deliberately skips.
mobxjs
mobx
- 28,201Stars
- 27Hotness
- 72Reliability
- over 11 yearsAge
mobxjs/mobx is transparent reactive state. You mark data observable — often via makeAutoObservable(this) in a class — mutate it directly with plain assignment, and MobX tracks which observers read which fields. Components wrapped in observer() re-render automatically when the exact observables they read change. The model is mutable and reaction-driven rather than action/reducer-based.
MobX's strengths: you write natural mutable code (this.count++) and reactivity is fully automatic with minimal boilerplate; its dependency tracking is fine-grained and efficient out of the box; and it scales well to rich object-oriented domain models. Drawbacks: the 'magic' tracking can obscure why something re-rendered; the mutable, non-serializable state makes time-travel and action logs harder than reduxjs/redux; and decorators or observer wrapping add setup and a learning curve that clashes with React's functional defaults.
Pick mobxjs/mobx when you have a complex, object-oriented domain model and want automatic reactivity without writing reducers. If you want serializable, auditable updates or a more idiomatic functional-React style, choose reduxjs/redux-toolkit or pmndrs/zustand instead.
reduxjs
redux
- 61,511Stars
- 22Hotness
- 53Reliability
- about 11 yearsAge
reduxjs/redux is the original predictable state container: one immutable store, plain-object actions, and pure reducer functions of (state, action) => newState. You dispatch({ type: 'counter/inc' }) and a reducer returns the next state; nothing mutates in place. Redux itself is framework-agnostic and small — the React glue lives separately in reduxjs/react-redux.
Redux's strengths: the one-way data flow is dead predictable and easy to reason about under load; every state change is a serializable action, so logging, replay, and time-travel debugging are trivial; and its middleware model makes side effects explicit. Drawbacks: hand-written Redux is famously verbose — action types, creators, and reducers for every field; it has no built-in async story; and for most apps reduxjs/redux-toolkit now supersedes raw Redux, which the maintainers themselves recommend.
Reach for raw reduxjs/redux only when you need the bare core or are teaching the pattern. For real applications use reduxjs/redux-toolkit, which is the same model with the boilerplate removed; if you just want simple shared state, pmndrs/zustand is far less code.
immerjs
immer
- 28,968Stars
- 7Hotness
- 34Reliability
- over 8 yearsAge
immerjs/immer is not a store — it's the immutable-update helper many stores use. You call produce(state, (draft) => { draft.items.push(x) }); you mutate the draft freely and Immer returns a new immutable state, structurally sharing the untouched parts. It powers createSlice in reduxjs/redux-toolkit and ships as middleware for pmndrs/zustand.
Immer's strengths: it replaces error-prone nested spread updates with readable mutable code; structural sharing keeps it efficient; and it's tiny and composable into any reducer or setter. Drawbacks: it's a utility, not a state manager, so it solves only the update step and nothing about subscriptions or storage; proxies add slight overhead on very large updates; and you must enable Map and Set support explicitly before using them.
Use immerjs/immer alongside whatever store you choose whenever immutable updates of deeply nested state get painful. If you're on reduxjs/redux-toolkit you already have it built in; you'd rarely pick Immer instead of a state manager — it complements pmndrs/zustand and reduxjs/redux rather than competing with them.
reduxjs
react-redux
- 23,467Stars
- 3Hotness
- 1Reliability
- about 11 yearsAge
reduxjs/react-redux is the official binding layer that connects a Redux store to React components — it's not a store itself. You wrap the tree in <Provider store={store}>, then read state with the useSelector hook and dispatch with useDispatch. It handles subscription, memoized selector comparison, and batching so components re-render only when their selected state changes.
react-redux's strengths: the hooks API is clean and it's battle-tested at huge scale; its selector memoization and update batching are carefully optimized; and it's maintained in lockstep with reduxjs/redux and reduxjs/redux-toolkit. Drawbacks: it's only useful if you're already using Redux, so it inherits Redux's ceremony; useSelector needs care to avoid over-rendering or unstable references; and standalone libraries like pmndrs/zustand fold the binding role into the store itself.
You use reduxjs/react-redux whenever you run Redux in React — it's mandatory glue, typically via reduxjs/redux-toolkit. If you're not committed to Redux, choose pmndrs/zustand instead, which needs no separate binding layer or provider.
facebookexperimental
Recoil
- 19,450Stars
facebookexperimental/Recoil is Meta's atom-and-selector state library for React. You define atom({ key, default }) for units of state and selector({ key, get }) for derived values, then read them with useRecoilState and useRecoilValue. It wraps your tree in a RecoilRoot and, like pmndrs/jotai, re-renders only components subscribed to changed atoms, with first-class async selectors.
Recoil's strengths: the atom/selector API is clean and integrates Suspense for async derived state; it pioneered fine-grained atomic subscriptions in React; and string keys make atoms addressable for persistence and debugging. Drawbacks: it remains explicitly experimental and development has largely stalled, which is a real risk for new projects; the required string keys add friction; and pmndrs/jotai delivers the same atom model with a smaller, more active, key-free API.
Only choose facebookexperimental/Recoil if you're already invested in it. For new work wanting the atomic model, pick pmndrs/jotai instead — it's actively maintained, lighter, and key-free; for general shared state, pmndrs/zustand.
pmndrs
valtio
- 10,210Stars
pmndrs/valtio is proxy-based state. You wrap a plain object with const state = proxy({ count: 0 }), mutate it anywhere with normal assignment (state.count++), and read it in components via const snap = useSnapshot(state). Valtio tracks which snapshot properties each component accesses and re-renders only those, combining MobX-style mutation with hook-based reads.
Valtio's strengths: writing mutable code while getting immutable snapshots is ergonomic and terse; access-based render tracking is automatic and fine-grained; and the same proxy works outside React for vanilla logic. Drawbacks: proxies can surprise you with deep-nesting and snapshot-identity edge cases; the implicit tracking makes re-renders harder to predict than zustand's explicit selectors; and it has a smaller community than the bigger libraries here.
Choose pmndrs/valtio when you like mutable, MobX-style writes but want React hooks and a tiny API. If you prefer explicit selectors and a single clear store, pick pmndrs/zustand; if you want atoms, pmndrs/jotai — all three come from the same pmndrs group.
rematch
rematch
- 8,410Stars
rematch/rematch is a framework built on reduxjs/redux that removes its boilerplate. You define a model with state, reducers, and effects in one object, and Rematch wires up the store, action creators, and async handling for you — no action constants or switch statements. It keeps the Redux core, so reduxjs/react-redux bindings and DevTools still work.
Rematch's strengths: models colocate state, sync reducers, and async effects with very little ceremony; async lives in effects without extra middleware like thunk or saga; and you keep full Redux DevTools and the surrounding Redux ecosystem. Drawbacks: reduxjs/redux-toolkit is now the official boilerplate-reducer and has far more momentum and resources; Rematch's community is comparatively small; and it still carries Redux's core concepts that lighter libraries avoid.
Pick rematch/rematch only if you specifically prefer its model-based API over Redux. For most teams reduxjs/redux-toolkit is the better-supported official path to the same goal, and pmndrs/zustand is lighter still if you don't need the Redux ecosystem.
didierfranc
react-waterfall
- 1,320Stars
didierfranc/react-waterfall is a small state store built on React's Context API. You define an initial state and actions, wrap your app in its Provider, and connect components to consume state and dispatch actions. It was an early, minimal take on getting Redux-like shared state from context with very little code.
react-waterfall's strengths: it's tiny and conceptually simple, and it demonstrated context-based stores before today's libraries existed. Drawbacks dominate now: it is effectively unmaintained, so it's a poor choice for new projects; context-based updates re-render broadly without the fine-grained selector subscriptions that pmndrs/zustand and pmndrs/jotai provide; and its small community means little support or tooling around it.
Don't pick didierfranc/react-waterfall for new work. Anything it does is handled better today by pmndrs/zustand for a tiny global store, or by raw React context for trivial cases — both are actively maintained and more efficient than react-waterfall.
