mobx, react-redux, recoil, redux, redux-saga, redux-thunk, xstate, and zustand are all libraries designed to manage application state in JavaScript applications, particularly those built with React. They address the challenge of sharing, synchronizing, and updating data across components while maintaining predictability and performance. redux provides a centralized store with immutable state updates via pure reducer functions. react-redux is the official React binding for Redux, enabling efficient component subscriptions. redux-thunk and redux-saga extend Redux to handle side effects like API calls—thunks with simple async functions, sagas with generator-based control flow. mobx uses observable state and automatic reactivity to track dependencies and update components. recoil offers a graph-based model with atoms (shared state) and selectors (derived state). zustand provides a lightweight, hook-centric store with minimal boilerplate. xstate implements finite state machines and statecharts for modeling complex, deterministic workflows.
Managing state in React apps goes beyond useState. When data must be shared across components, survive re-renders, or coordinate complex workflows, you need a dedicated solution. The libraries here offer different philosophies: some enforce strict immutability (redux), others embrace reactivity (mobx), while some model behavior explicitly (xstate). Let’s compare them through real engineering lenses.
redux treats state as immutable snapshots. Every change produces a new state object via pure reducer functions. This enables powerful debugging but requires discipline.
// redux: Immutable updates via reducers
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
default:
return state;
}
};
mobx uses observable objects. Changes to state automatically notify observers (like React components), with mutations allowed directly.
// mobx: Mutable state with automatic tracking
import { makeAutoObservable } from 'mobx';
class Counter {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++; // Direct mutation
}
}
recoil models state as a directed graph of atoms (units of state) and selectors (computed values). Components subscribe only to what they use.
// recoil: Atoms and selectors
import { atom, selector } from 'recoil';
const countAtom = atom({ key: 'count', default: 0 });
const doubledCount = selector({
key: 'doubledCount',
get: ({ get }) => get(countAtom) * 2
});
zustand provides a single store hook with mutable state. No providers, no actions—just a function returning state and setters.
// zustand: Minimal store
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
xstate defines state as explicit machines with finite states and transitions. Behavior is declared upfront, preventing invalid flows.
// xstate: State machine
import { createMachine } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
When your state depends on API calls or timers, how you manage side effects matters.
redux-thunk lets action creators return functions that dispatch actions asynchronously.
// redux-thunk: Simple async
const fetchUser = (id) => async (dispatch) => {
dispatch({ type: 'FETCH_START' });
try {
const user = await api.getUser(id);
dispatch({ type: 'FETCH_SUCCESS', payload: user });
} catch (err) {
dispatch({ type: 'FETCH_ERROR', error: err });
}
};
redux-saga uses generator functions to describe complex async flows declaratively.
// redux-saga: Complex async with cancellation
import { call, put, takeEvery, cancel } from 'redux-saga/effects';
function* fetchUserSaga(action) {
const task = yield fork(api.getUser, action.payload.id);
yield take('CANCEL_FETCH');
yield cancel(task);
}
function* watchFetchUser() {
yield takeEvery('FETCH_USER', fetchUserSaga);
}
mobx allows async methods directly in stores, often using async/await.
// mobx: Async in store
import { makeAutoObservable, runInAction } from 'mobx';
class UserStore {
user = null;
loading = false;
constructor() {
makeAutoObservable(this);
}
async fetchUser(id) {
this.loading = true;
try {
const user = await api.getUser(id);
runInAction(() => {
this.user = user;
this.loading = false;
});
} catch (err) {
// handle error
}
}
}
recoil supports async selectors that automatically manage loading and error states.
// recoil: Async selector
const userQuery = selector({
key: 'userQuery',
get: async ({ get }) => {
const id = get(userIdAtom);
const response = await fetch(`/api/user/${id}`);
return response.json();
}
});
zustand handles async directly in actions, similar to MobX.
// zustand: Async in store
const useStore = create((set) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true });
try {
const user = await api.getUser(id);
set({ user, loading: false });
} catch (err) {
// handle error
}
}
}));
xstate integrates async via invoke and transition guards.
// xstate: Async in machine
const fetchUserMachine = createMachine({
id: 'fetchUser',
initial: 'idle',
context: { user: null },
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
invoke: {
src: 'getUser',
onDone: { target: 'success', actions: 'assignUser' },
onError: 'failure'
}
},
success: {},
failure: {}
}
}, {
services: {
getUser: (context, event) => api.getUser(event.id)
},
actions: {
assignUser: assign({ user: (context, event) => event.data })
}
});
react-redux requires a <Provider> and uses hooks to connect components.
// react-redux: Component usage
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<button onClick={() => dispatch({ type: 'increment' })}>
{count}
</button>
);
}
mobx uses observer to wrap components, which auto-track used observables.
// mobx: Observer component
import { observer } from 'mobx-react-lite';
const Counter = observer(({ store }) => (
<button onClick={() => store.increment()}>
{store.count}
</button>
));
recoil uses hooks directly—no providers needed in most cases.
// recoil: Hook usage
import { useRecoilState, useRecoilValue } from 'recoil';
function Counter() {
const [count, setCount] = useRecoilState(countAtom);
const doubled = useRecoilValue(doubledCount);
return <div>{doubled}</div>;
}
zustand uses its store hook directly in components.
// zustand: Hook usage
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
xstate uses useMachine to instantiate and interact with machines.
// xstate: Hook usage
import { useMachine } from '@xstate/react';
function Toggle() {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.value === 'active' ? 'ON' : 'OFF'}
</button>
);
}
redux shines with the Redux DevTools extension, offering time-travel debugging, action inspection, and state diffing. Middleware like redux-logger adds console logging.
mobx offers mobx-devtools for tracking observables and reactions, though less mature than Redux’s tooling. Strict mode helps catch accidental mutations.
recoil has experimental DevTools that visualize the state graph and dependencies, but adoption is limited due to Meta’s reduced investment.
zustand provides minimal built-in tooling but integrates with Redux DevTools via middleware. Its simplicity often reduces the need for advanced debugging.
xstate includes the XState Inspector for visualizing state machines and transitions, invaluable for complex workflows.
redux, react-redux, recoil (atoms are replaced, not mutated)mobx, zustand (though zustand encourages immutable updates via set)xstate (context can be mutable, but state transitions are immutable)This choice affects performance, debugging, and mental model. Immutability prevents accidental side effects but requires more careful coding. Mutability feels natural but risks unexpected updates if not managed.
These aren’t always mutually exclusive:
redux + redux-saga for complex enterprise apps needing audit trails.xstate for UI workflow logic (e.g., checkout steps) alongside zustand for cached data.mobx for local component state and recoil for global config if migrating incrementally.Avoid mixing too many—each adds cognitive overhead.
| Library | State Model | Async Handling | React Integration | Mutability | Best For |
|---|---|---|---|---|---|
redux | Centralized store | Middleware (thunk/saga) | react-redux | Immutable | Large apps needing strict predictability |
react-redux | Binding only | N/A | Required for Redux | N/A | Connecting Redux to React |
redux-thunk | Middleware | Simple async | With Redux | N/A | Basic API calls in Redux apps |
redux-saga | Middleware | Complex async | With Redux | N/A | Advanced async workflows in Redux |
mobx | Observable objects | Async methods | observer HOC | Mutable | Reactive apps with minimal boilerplate |
recoil | Atom graph | Async selectors | Hooks | Immutable | Fine-grained state with derived data |
zustand | Single store | Async actions | Hooks | Mutable* | Lightweight global state without providers |
xstate | State machines | Invoke/services | useMachine | Hybrid | Explicit, deterministic UI flows |
zustand or recoil reduce boilerplate without sacrificing power.redux and its ecosystem.xstate prevents bugs by design.mobx feels intuitive but requires discipline.Choose based on your team’s familiarity, app complexity, and long-term maintenance needs—not hype.
Choose mobx if you prefer a reactive programming model where state changes automatically trigger UI updates without manual subscription management. It’s ideal for teams comfortable with mutable state and seeking to minimize boilerplate, especially in medium-to-large apps where fine-grained reactivity improves performance. However, be prepared to adopt strict mode or use makeAutoObservable to avoid common pitfalls with observability.
Choose react-redux if you’re already using Redux and need the official, optimized React integration that handles store subscription, memoization, and context efficiently. It’s essential for any Redux-based React app, providing hooks like useSelector and useDispatch. Don’t use it standalone—it’s a binding layer, not a state management solution by itself.
Choose recoil if you want a modern, React-native approach to global state with built-in support for derived state, async selectors, and concurrent rendering. It works well for apps needing fine-grained updates and data-flow graphs, but note that its future is uncertain as Meta has shifted focus to React Server Components, so consider long-term maintenance risks.
Choose redux if you need a predictable, centralized state container with strong dev tools, middleware support, and time-travel debugging. It’s best suited for large applications requiring strict state immutability, auditability, and team-wide consistency. Be aware that it demands more boilerplate and learning overhead compared to newer alternatives.
Choose redux-saga if your Redux app involves complex side effects like long-running tasks, race conditions, or cancellation logic that thunks can’t easily express. It uses ES6 generators to manage control flow declaratively, making testable and maintainable async logic. Only use it alongside redux—it’s not a standalone state manager.
Choose redux-thunk if your Redux app needs simple asynchronous logic like basic API calls that dispatch actions based on responses. It’s the easiest Redux middleware to learn and integrates seamlessly with standard Redux patterns. Avoid it for complex workflows involving multiple dependent async operations or cancellation—opt for redux-saga instead.
Choose xstate if your application logic is best modeled as explicit state machines or statecharts, such as multi-step wizards, complex UI flows, or protocol-driven interactions. It enforces deterministic transitions and prevents invalid states, improving reliability. It’s not a general-purpose state store—use it for behavior modeling, not data caching.
Choose zustand if you want a minimal, hook-based global state solution with zero boilerplate, no context providers, and excellent performance out of the box. It’s perfect for small-to-medium apps or teams migrating away from Redux who still need shared state. Avoid it if you require advanced dev tools, middleware ecosystems, or strict immutability guarantees.
Simple, scalable state management.
Documentation can be found at mobx.js.org.
MobX is made possible by the generosity of the sponsors below, and many other individual backers. Sponsoring directly impacts the longevity of this project.
🥇🥇 Platinum sponsors ($5000+ total contribution): 🥇🥇
🥇 Gold sponsors ($2500+ total contribution):
🥈 Silver sponsors ($500+ total contributions):
Anything that can be derived from the application state, should be. Automatically.
MobX is a signal based, battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming. The philosophy behind MobX is simple:
Write minimalistic, boilerplate-free code that captures your intent. Trying to update a record field? Simply use a normal JavaScript assignment — the reactivity system will detect all your changes and propagate them out to where they are being used. No special tools are required when updating data in an asynchronous process.
All changes to and uses of your data are tracked at runtime, building a dependency tree that captures all relations between state and output. This guarantees that computations that depend on your state, like React components, run only when strictly needed. There is no need to manually optimize components with error-prone and sub-optimal techniques like memoization and selectors.
MobX is unopinionated and allows you to manage your application state outside of any UI framework. This makes your code decoupled, portable, and above all, easily testable.
So what does code that uses MobX look like?
import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react-lite"
// Model the application state.
function createTimer() {
return makeAutoObservable({
secondsPassed: 0,
increase() {
this.secondsPassed += 1
},
reset() {
this.secondsPassed = 0
}
})
}
const myTimer = createTimer()
// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
<button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))
ReactDOM.render(<TimerView timer={myTimer} />, document.body)
// Update the 'Seconds passed: X' text every second.
setInterval(() => {
myTimer.increase()
}, 1000)
The observer wrapper around the TimerView React component will automatically detect that rendering
depends on the timer.secondsPassed observable, even though this relationship is not explicitly defined. The reactivity system will take care of re-rendering the component when precisely that field is updated in the future.
Every event (onClick / setInterval) invokes an action (myTimer.increase / myTimer.reset) that updates observable state (myTimer.secondsPassed).
Changes in the observable state are propagated precisely to all computations and side effects (TimerView) that depend on the changes being made.
This conceptual picture can be applied to the above example, or any other application using MobX.
To learn about the core concepts of MobX using a larger example, check out The gist of MobX page, or take the 10 minute interactive introduction to MobX and React. The philosophy and benefits of the mental model provided by MobX are also described in great detail in the blog posts UI as an afterthought and How to decouple state and UI (a.k.a. you don’t need componentWillMount).
The MobX Quick Start Guide ($24.99) by Pavan Podila and Michel Weststrate is available as an ebook, paperback, and on the O'Reilly platform (see preview).
MobX is inspired by reactive programming principles, which are for example used in spreadsheets. It is inspired by model–view–viewmodel frameworks like MeteorJS's Tracker, Knockout and Vue.js, but MobX brings transparent functional reactive programming (TFRP, a concept which is further explained in the MobX book) to the next level and provides a standalone implementation. It implements TFRP in a glitch-free, synchronous, predictable and efficient manner.
A ton of credit goes to Mendix, for providing the flexibility and support to maintain MobX and the chance to prove the philosophy of MobX in a real, complex, performance critical applications.