@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.
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.
@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();
All libraries integrate via React hooks, but their re-render strategies differ:
Provider + useSelector. Re-renders occur when selected state changes (shallow equality).observer HOC or useObserver hook. Tracks which observables a component reads.RecoilRoot Provider. Fine-grained subscriptions prevent unnecessary renders.useSnapshot hook. Creates a frozen snapshot; mutations trigger re-renders only for changed paths.useMachine hook returns current state and send function. Re-renders on state transitions.subscribeWithSelector middleware for granular updates.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 });
}
}));
jotai/devtools. Atoms are testable in isolation.While exact numbers vary, the general trade-offs are:
Performance-wise, all avoid unnecessary re-renders through memoization or fine-grained subscriptions—except basic Zustand usage, which may require middleware for optimization.
As of 2024:
Despite differences, these libraries share common goals:
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
Each enforces a clear update mechanism (actions, mutations, transitions) to prevent chaotic state changes.
Memoization, selective subscriptions, or proxy-based reactivity minimize wasted renders.
State logic is extractable from components, enabling unit tests without DOM rendering.
Most support plugins for persistence, logging, or devtools (e.g., Redux middleware, Zustand middleware).
| Library | State Model | Boilerplate | Learning Curve | Best For |
|---|---|---|---|---|
@reduxjs/toolkit | Centralized, immutable | Medium | Medium | Large apps, teams needing strict contracts |
jotai | Atomic, composable | Low | Low | Apps wanting React-like simplicity with scalability |
mobx | Observable objects | Low | Medium | Object-oriented domains, mutable preferences |
recoil | Atoms + selectors | Medium | Medium | Data-heavy UIs (if accepting maintenance status) |
valtio | Proxy-mutable | Very Low | Low | Rapid prototyping, direct mutation fans |
xstate | State machines | High | High | Complex workflows with strict state rules |
zustand | Hook-based store | Very Low | Low | Small/medium apps avoiding context/providers |
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.
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.
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.
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.
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.
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.
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.
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.
The official, opinionated, batteries-included toolset for efficient Redux development
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:
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.
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.
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:
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.
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 promisecreateEntityAdapter(): generates a set of reusable reducers and selectors to manage normalized data in the storecreateSelector() 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 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'
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.
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.