@reduxjs/toolkit vs jotai vs mobx vs recoil vs valtio vs xstate vs zustand
State Management Solutions for Modern React Applications
@reduxjs/toolkitjotaimobxrecoilvaltioxstatezustandSimilar Packages:

State Management Solutions for Modern React Applications

@reduxjs/toolkit, jotai, mobx, recoil, valtio, xstate, and zustand are all libraries designed to manage application state in JavaScript applications, particularly React. They address the limitations of React's built-in useState/useReducer by offering scalable, predictable, or reactive patterns for sharing and updating state across components. While Redux Toolkit provides a structured, centralized store with immutability guarantees, libraries like Zustand and Jotai offer simpler, hook-based APIs with minimal boilerplate. MobX and Valtio use proxy-based reactivity for mutable-like syntax, Recoil introduces atom-based state graphs with selectors, and XState models state as explicit finite state machines for complex workflows.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@reduxjs/toolkit011,1887.04 MB2613 months agoMIT
jotai021,063519 kB69 days agoMIT
mobx028,1834.35 MB806 months agoMIT
recoil019,5202.21 MB3223 years agoMIT
valtio010,147101 kB416 days agoMIT
xstate029,3382.25 MB152a month agoMIT
zustand057,45995 kB43 days agoMIT

State Management Deep Dive: Redux Toolkit vs Jotai vs MobX vs Recoil vs Valtio vs XState vs Zustand

Managing state in React apps goes beyond useState. When components need to share data, avoid prop drilling, or handle side effects predictably, dedicated state libraries become essential. The seven packages compared here represent distinct philosophies: from Redux’s disciplined immutability to XState’s formal state machines. Let’s explore how they solve real problems.

🧠 Core Mental Models: How Each Library Thinks About State

@reduxjs/toolkit treats state as a single, immutable tree updated by pure reducer functions. Changes happen via dispatched actions, enabling time-travel debugging and middleware.

// Redux Toolkit slice
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1; // Immer enables "mutable" syntax
    }
  }
});

export const { increment } = counterSlice.actions;
export default counterSlice.reducer;

jotai models state as independent atoms that can be read, written, or derived. Atoms compose like React hooks, avoiding global context.

// Jotai atom
import { atom } from 'jotai';

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

// In component
const [count, setCount] = useAtom(countAtom);

mobx wraps state in observable objects. Components using observer automatically re-render when observed values change.

// MobX store
import { makeAutoObservable } from 'mobx';

class CounterStore {
  count = 0;
  constructor() {
    makeAutoObservable(this);
  }
  increment() {
    this.count++;
  }
}

const store = new CounterStore();

// In component
const Counter = observer(() => <div>{store.count}</div>);

recoil uses atoms (shared state units) and selectors (derived state). Components subscribe only to atoms they use.

// Recoil atom
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';

const countState = atom({ key: 'count', default: 0 });
const doubledCountState = selector({
  key: 'doubledCount',
  get: ({ get }) => get(countState) * 2
});

// In component
const [count, setCount] = useRecoilState(countState);
const doubled = useRecoilValue(doubledCountState);

valtio creates a proxy-wrapped state object. You mutate it directly, and components using useSnapshot re-render on changes.

// Valtio state
import { proxy, useSnapshot } from 'valtio';

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

// In component
const snap = useSnapshot(state);
// Mutate anywhere: state.count++

xstate defines state as a finite state machine with explicit states and transitions. Guards and actions control behavior.

// XState machine
import { createMachine } from 'xstate';

const toggleMachine = createMachine({
  id: 'toggle',
  initial: 'inactive',
  states: {
    inactive: { on: { TOGGLE: 'active' } },
    active: { on: { TOGGLE: 'inactive' } }
  }
});

// In component
const [state, send] = useMachine(toggleMachine);

zustand provides a hook-based store. You define state and actions in one function, and components subscribe selectively.

// Zustand store
import { create } from 'zustand';

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

// In component
const { count, increment } = useStore();

🔌 Integration with React: Hooks, Context, and Re-renders

All libraries integrate via React hooks, but their re-render strategies differ:

  • Redux Toolkit: Requires Provider + useSelector. Re-renders occur when selected state changes (shallow equality).
  • Jotai: No Provider needed. Only components using an atom re-render when it updates.
  • MobX: Uses observer HOC or useObserver hook. Tracks which observables a component reads.
  • Recoil: Needs RecoilRoot Provider. Fine-grained subscriptions prevent unnecessary renders.
  • Valtio: Uses useSnapshot hook. Creates a frozen snapshot; mutations trigger re-renders only for changed paths.
  • XState: useMachine hook returns current state and send function. Re-renders on state transitions.
  • Zustand: No Provider. Components re-render when any part of the store changes unless you use subscribeWithSelector middleware for granular updates.

🔄 Async Logic: Handling Side Effects

Redux Toolkit uses createAsyncThunk for standardized async flows:

const fetchUser = createAsyncThunk('user/fetch', async (id) => {
  const res = await api.getUser(id);
  return res.data;
});

Jotai handles async in write atoms or via useEffect:

const fetchUserAtom = atom(null, async (_get, set, id) => {
  const user = await api.getUser(id);
  set(userAtom, user);
});

MobX encourages async in store methods:

class UserStore {
  async fetchUser(id) {
    this.user = await api.getUser(id);
  }
}

Recoil uses selectorFamily with async or useRecoilCallback:

const userQuery = selectorFamily({
  key: 'userQuery',
  get: (id) => async () => {
    const res = await api.getUser(id);
    return res.data;
  }
});

Valtio allows direct async mutations:

const fetchUser = async (id) => {
  state.user = await api.getUser(id);
};

XState embeds async in service actions:

states: {
  loading: {
    invoke: {
      src: 'fetchUser',
      onDone: { target: 'success', actions: 'assignUser' }
    }
  }
}

Zustand handles async in actions:

const useStore = create((set) => ({
  fetchUser: async (id) => {
    const user = await api.getUser(id);
    set({ user });
  }
}));

🧪 Testing and Debugging

  • Redux Toolkit: Excellent devtools support (time-travel, action history). Reducers are pure functions—easy to unit test.
  • Jotai: Devtools available via jotai/devtools. Atoms are testable in isolation.
  • MobX: Devtools extension shows observable changes. Testing requires mocking or snapshotting observable state.
  • Recoil: Built-in devtools for atom inspection. Selectors are pure—easy to test.
  • Valtio: Limited devtools; relies on console logging. Snapshots simplify testing.
  • XState: Visualizer tool generates state diagrams. Machines are deterministic—highly testable.
  • Zustand: Basic devtools middleware. Stores are plain objects—simple to mock.

📦 Bundle Size and Performance

While exact numbers vary, the general trade-offs are:

  • Lightest: Zustand, Valtio, Jotai (under 5KB minified)
  • Moderate: MobX, Recoil
  • Heaviest: Redux Toolkit (includes Immer, Redux core), XState (full statechart engine)

Performance-wise, all avoid unnecessary re-renders through memoization or fine-grained subscriptions—except basic Zustand usage, which may require middleware for optimization.

🚫 Deprecated or Uncertain Futures?

As of 2024:

  • Recoil is in maintenance mode. Meta recommends exploring React Server Components instead for new projects, though it remains stable.
  • All others are actively maintained with regular releases.

🤝 Shared Strengths Across Libraries

Despite differences, these libraries share common goals:

1. 🧩 Avoid Prop Drilling

All enable cross-component state access without manual prop passing.

// Instead of <Child count={count} setCount={setCount} />
// Use any library's hook directly in Child

2. 🔄 Predictable Updates

Each enforces a clear update mechanism (actions, mutations, transitions) to prevent chaotic state changes.

3. ⚡ Performance Optimizations

Memoization, selective subscriptions, or proxy-based reactivity minimize wasted renders.

4. 🧪 Testability

State logic is extractable from components, enabling unit tests without DOM rendering.

5. 🔌 Middleware/Extensibility

Most support plugins for persistence, logging, or devtools (e.g., Redux middleware, Zustand middleware).

📊 Summary Table

LibraryState ModelBoilerplateLearning CurveBest For
@reduxjs/toolkitCentralized, immutableMediumMediumLarge apps, teams needing strict contracts
jotaiAtomic, composableLowLowApps wanting React-like simplicity with scalability
mobxObservable objectsLowMediumObject-oriented domains, mutable preferences
recoilAtoms + selectorsMediumMediumData-heavy UIs (if accepting maintenance status)
valtioProxy-mutableVery LowLowRapid prototyping, direct mutation fans
xstateState machinesHighHighComplex workflows with strict state rules
zustandHook-based storeVery LowLowSmall/medium apps avoiding context/providers

💡 Final Guidance

  • Start simple: For most new apps, try Zustand or Jotai first—they offer the best balance of power and simplicity.
  • Need structure? Choose Redux Toolkit if your team values explicit contracts and debugging superpowers.
  • Modeling workflows? XState prevents bugs in complex UI logic but requires upfront design.
  • Prefer mutation? MobX or Valtio let you write intuitive code while staying reactive.
  • Avoid Recoil for new greenfield projects given its maintenance status—unless you’re already invested.

The right choice depends less on features and more on your team’s mental model, app complexity, and tolerance for boilerplate. All these libraries solve real problems—pick the one that feels least like friction for your workflow.

How to Choose: @reduxjs/toolkit vs jotai vs mobx vs recoil vs valtio vs xstate vs zustand

  • @reduxjs/toolkit:

    Choose @reduxjs/toolkit if you need a battle-tested, centralized state architecture with strong devtools support, middleware ecosystem (like Redux Thunk or Saga), and strict immutability. It’s ideal for large teams requiring predictable state transitions, time-travel debugging, or integration with existing Redux patterns — though it comes with more boilerplate than newer alternatives.

  • jotai:

    Choose jotai if you prefer a minimal, atomic state model that composes well with React’s concurrent features and avoids global context bottlenecks. Its atom-based design scales from simple local state to complex derived computations without wrapper components, making it great for medium-sized apps where you want React-like ergonomics with better performance than Context API.

  • mobx:

    Choose mobx if you favor writing mutable-style code that automatically tracks dependencies and re-renders only affected components. It excels in apps with highly dynamic, object-oriented domain models where imperative updates feel natural — but requires careful attention to observer boundaries and may obscure data flow for newcomers.

  • recoil:

    Choose recoil if you want fine-grained reactivity with atoms and selectors that integrate deeply with React’s rendering lifecycle. It’s well-suited for data-heavy UIs (like dashboards) where components subscribe to specific slices of state, though its future is uncertain as Meta has shifted focus to React Server Components.

  • valtio:

    Choose valtio if you like working with plain JavaScript objects but still want automatic reactivity and minimal setup. It uses Proxies to make mutable-looking code reactive, offering a middle ground between Redux’s immutability and MobX’s observables — ideal for rapid prototyping or teams comfortable with direct object mutation.

  • xstate:

    Choose xstate when your UI logic involves complex, well-defined workflows with discrete states and transitions (e.g., multi-step forms, wizards, or device control panels). Its visualizable statecharts prevent invalid states and race conditions, but introduce a steeper learning curve for teams unfamiliar with formal state machine concepts.

  • zustand:

    Choose zustand if you want a lightweight, hook-centric store with zero boilerplate and no context providers. It’s perfect for small-to-medium apps needing shared state without Redux’s ceremony, and supports async actions, middleware, and partial subscriptions out of the box — though it lacks built-in devtools integration.

README for @reduxjs/toolkit

Redux Toolkit

GitHub Workflow Status npm version npm downloads

The official, opinionated, batteries-included toolset for efficient Redux development

Installation

Create a React Redux App

The recommended way to start new apps with React and Redux Toolkit is by using our official Redux Toolkit + TS template for Vite, or by creating a new Next.js project using Next's with-redux template.

Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.

# Vite with our Redux+TS template
# (using the `degit` tool to clone and extract the template)
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app

# Next.js using the `with-redux` template
npx create-next-app --example with-redux my-app

We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:

An Existing App

Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:

# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

The package includes a precompiled ESM build that can be used as a <script type="module"> tag directly in the browser.

Documentation

The Redux Toolkit docs are available at https://redux-toolkit.js.org, including API references and usage guides for all of the APIs included in Redux Toolkit.

The Redux core docs at https://redux.js.org includes the full Redux tutorials, as well usage guides on general Redux patterns.

Purpose

The Redux Toolkit package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:

  • "Configuring a Redux store is too complicated"
  • "I have to add a lot of packages to get Redux to do anything useful"
  • "Redux requires too much boilerplate code"

We can't solve every use case, but in the spirit of create-react-app, we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.

Because of that, this package is deliberately limited in scope. It does not address concepts like "reusable encapsulated Redux modules", folder or file structures, managing entity relationships in the store, and so on.

Redux Toolkit also includes a powerful data fetching and caching capability that we've dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.

What's Included

Redux Toolkit includes these APIs:

  • configureStore(): wraps createStore to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add whatever Redux middleware you supply, includes redux-thunk by default, and enables use of the Redux DevTools Extension.
  • createReducer(): lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the immer library to let you write simpler immutable updates with normal mutative code, like state.todos[3].completed = true.
  • createAction(): generates an action creator function for the given action type string. The function itself has toString() defined, so that it can be used in place of the type constant.
  • createSlice(): combines createReducer() + createAction(). Accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
  • combineSlices(): combines multiple slices into a single reducer, and allows "lazy loading" of slices after initialisation.
  • createListenerMiddleware(): lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. A lightweight alternative to Redux async middleware like sagas and observables.
  • createAsyncThunk(): accepts an action type string and a function that returns a promise, and generates a thunk that dispatches pending/resolved/rejected action types based on that promise
  • createEntityAdapter(): generates a set of reusable reducers and selectors to manage normalized data in the store
  • The createSelector() utility from the Reselect library, re-exported for ease of use.

For details, see the Redux Toolkit API Reference section in the docs.

RTK Query

RTK Query is provided as an optional addon within the @reduxjs/toolkit package. It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer for your app. It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.

RTK Query is built on top of the Redux Toolkit core for its implementation, using Redux internally for its architecture. Although knowledge of Redux and RTK are not required to use RTK Query, you should explore all of the additional global store management capabilities they provide, as well as installing the Redux DevTools browser extension, which works flawlessly with RTK Query to traverse and replay a timeline of your request & cache behavior.

RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:

import { createApi } from '@reduxjs/toolkit/query'

/* React-specific entry point that automatically generates
   hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react'

What's included

RTK Query includes these APIs:

  • createApi(): The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.
  • fetchBaseQuery(): A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.
  • <ApiProvider />: Can be used as a Provider if you do not already have a Redux store.
  • setupListeners(): A utility used to enable refetchOnMount and refetchOnReconnect behaviors.

See the RTK Query Overview page for more details on what RTK Query is, what problems it solves, and how to use it.

Contributing

Please refer to our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Redux Toolkit.