immer, mobx, recoil, redux, valtio, and zustand are all libraries designed to help manage application state in JavaScript applications, particularly in React ecosystems. They address the challenge of keeping UI synchronized with data while offering different mental models, performance characteristics, and developer ergonomics. redux provides a predictable, centralized store with strict unidirectional data flow. immer enables immutable updates through mutable syntax by leveraging proxies. mobx uses observables and reactions for automatic reactivity. recoil offers atom-based state with selectors for derived state, built specifically for React. zustand delivers a lightweight, hook-based global store with minimal boilerplate. valtio combines proxy-based reactivity with a simple API that integrates naturally with React’s rendering model.
Managing state in JavaScript applications — especially React apps — is a core challenge. The libraries immer, mobx, recoil, redux, valtio, and zustand each offer distinct approaches. Let’s compare them across key dimensions: mutation style, reactivity model, React integration, and real-world usage patterns.
immer isn’t a state manager — it’s an immutable update helper. You write code as if mutating an object, but it produces a new immutable copy under the hood using Proxies.
import produce from 'immer';
const nextState = produce(baseState, (draft) => {
draft.user.name = 'Alice'; // looks mutable, but returns new object
});
mobx treats state as observable objects. Components automatically re-render when observed values change, thanks to fine-grained reactive tracking.
import { makeAutoObservable } from 'mobx';
class Store {
user = { name: 'Bob' };
constructor() { makeAutoObservable(this); }
updateName(name) { this.user.name = name; }
}
recoil models state as atoms (units of state) and selectors (derived state). It’s built for React and leverages React’s concurrent features.
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
const userAtom = atom({ key: 'user', default: { name: 'Charlie' } });
const userNameSelector = selector({
key: 'userName',
get: ({ get }) => get(userAtom).name
});
redux enforces a single immutable store, updated only by pure reducer functions in response to dispatched actions.
// With Redux Toolkit
import { createSlice } from '@reduxjs/toolkit';
const userSlice = createSlice({
name: 'user',
initialState: { name: 'David' },
reducers: {
updateName: (state, action) => {
state.name = action.payload; // immer-powered mutation!
}
}
});
valtio uses a proxy-based store where you mutate state directly, and components subscribe to snapshots of that state.
import { proxy, useSnapshot } from 'valtio';
const state = proxy({ user: { name: 'Eve' } });
function Component() {
const snap = useSnapshot(state);
return <div>{snap.user.name}</div>;
}
zustand provides a hook-based store with direct state access and optional middleware. No providers needed.
import { create } from 'zustand';
const useStore = create((set) => ({
user: { name: 'Frank' },
updateName: (name) => set((state) => ({ user: { ...state.user, name } }))
}));
function Component() {
const { user, updateName } = useStore();
return <div>{user.name}</div>;
}
Each library uses a different mechanism to decide when components should update.
immer: No built-in reactivity. Used alongside other systems (e.g., Redux) that handle updates.mobx: Tracks which observables a component reads during render. Only re-renders when those specific observables change.recoil: Subscribes components to specific atoms/selectors. Updates only when subscribed values change.redux: By default, re-renders on every store update unless you use React.memo or createSelector to optimize.valtio: Uses useSnapshot to create a reactive view of the proxy. Only properties accessed in render are tracked for updates.zustand: By default, the whole component re-renders on any state change. But you can select specific slices to minimize re-renders.// Zustand: Select only what you need
const userName = useStore(state => state.user.name);
This selective subscription is crucial for performance in larger apps.
Let’s compare how much code you need for a basic counter.
immer (used with useState):
import { useState } from 'react';
import produce from 'immer';
function Counter() {
const [state, setState] = useState({ count: 0 });
const increment = () => setState(produce(draft => { draft.count += 1; }));
return <div>{state.count}</div>;
}
mobx:
import { observer } from 'mobx-react-lite';
import { makeAutoObservable } from 'mobx';
class CounterStore {
count = 0;
constructor() { makeAutoObservable(this); }
increment = () => { this.count += 1; };
}
const store = new CounterStore();
const Counter = observer(() => (
<div onClick={store.increment}>{store.count}</div>
));
recoil:
import { atom, useRecoilState } from 'recoil';
const countAtom = atom({ key: 'count', default: 0 });
function Counter() {
const [count, setCount] = useRecoilState(countAtom);
return <div onClick={() => setCount(c => c + 1)}>{count}</div>;
}
redux (with Redux Toolkit):
import { createSlice, configureStore } from '@reduxjs/toolkit';
import { Provider, useSelector, useDispatch } from 'react-redux';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: { increment: (state) => { state.count += 1; } }
});
const store = configureStore({ reducer: counterSlice.reducer });
function Counter() {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return <div onClick={() => dispatch(counterSlice.actions.increment())}>{count}</div>;
}
valtio:
import { proxy, useSnapshot } from 'valtio';
const state = proxy({ count: 0 });
function Counter() {
const snap = useSnapshot(state);
return <div onClick={() => ++state.count}>{snap.count}</div>;
}
zustand:
import { create } from 'zustand';
const useCounter = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
function Counter() {
const { count, increment } = useCounter();
return <div onClick={increment}>{count}</div>;
}
Zustand and Valtio require the least boilerplate. Redux demands the most setup but pays off in tooling and predictability.
immer: No async or middleware. Pure utility.mobx: Supports async actions via flow or async/await. DevTools available via mobx-devtools.recoil: Built-in async support with selector and waitForAll. DevTools via browser extension.redux: Rich middleware ecosystem (thunk, saga, RTK Query). Excellent DevTools with time-travel.valtio: Async works naturally with mutations. DevTools via valtio/devtools.zustand: Middleware for persist, devtools, immer, etc. Async handled in actions. DevTools middleware available.Example: Async data fetching in each (where applicable).
mobx:
import { flow } from 'mobx';
class UserStore {
user = null;
fetchUser = flow(function* (id) {
this.user = yield fetch(`/api/user/${id}`).then(r => r.json());
});
}
recoil:
const userQuery = selector({
key: 'userQuery',
get: async ({ get }) => {
const id = get(userIdAtom);
const res = await fetch(`/api/user/${id}`);
return res.json();
}
});
redux (RTK Query):
// Typically handled via RTK Query endpoints, not shown here for brevity
valtio:
async function fetchUser(id) {
state.user = await fetch(`/api/user/${id}`).then(r => r.json());
}
zustand:
const useStore = create((set) => ({
user: null,
fetchUser: async (id) => {
const user = await fetch(`/api/user/${id}`).then(r => r.json());
set({ user });
}
}));
As of 2024:
recoil is in maintenance mode. Meta announced it will not be actively developed, though it remains stable. New projects should evaluate alternatives.immer, mobx, redux, valtio, zustand) are actively maintained with regular releases.Do not start new projects with recoil unless you have a compelling reason and accept the risk of long-term stagnation.
| Scenario | Best Fit |
|---|---|
| Need immutable updates with clean syntax | immer (as helper) |
| Complex app with lots of derived state and fine-grained updates | mobx |
| Existing Recoil project or perfect fit for atom model | recoil (but caution for new projects) |
| Large team, need strict structure, time-travel debugging | redux + RTK |
| Small-to-medium app, want proxy-based reactivity | valtio |
| Most React apps needing global state with minimal fuss | zustand |
There’s no one-size-fits-all. But for new React projects in 2024, zustand is often the sweet spot: simple, performant, and flexible. If you need more structure or are in a large team, redux with Redux Toolkit remains a solid choice. Use immer whenever you’re doing immutable updates by hand. Pick mobx if your team embraces its reactivity model. Avoid recoil for new greenfield projects. And valtio is a great dark horse for teams that like proxies and minimal APIs.
Choose based on your team’s preferences, app size, and long-term maintainability — not just hype.
Choose immer when you need to work with immutable data structures but prefer writing code that looks mutable. It pairs well with Redux or other immutable-first systems to reduce boilerplate. Avoid using it as a standalone state management solution — it’s a utility for producing immutable updates, not a full state architecture.
Choose redux when you need a battle-tested, predictable state container with strong devtooling, middleware support, and a clear data flow. It’s best suited for large teams or complex applications where time-travel debugging and strict structure are valuable. Use it with Redux Toolkit to avoid excessive boilerplate; avoid vanilla Redux patterns.
Choose zustand when you need a simple, hook-based global store with zero boilerplate and good performance. It’s perfect for most React apps that outgrow useState but don’t need Redux’s complexity. It supports middleware, async actions, and partial subscriptions out of the box, making it a versatile default choice for new projects.
Choose mobx when you want automatic reactivity with minimal setup and your team is comfortable with observable patterns. It excels in complex applications where fine-grained updates and derived computations are common. Be aware that its magic-like reactivity can make debugging harder if developers don’t understand its tracking system.
Choose valtio when you want proxy-based reactivity with a minimal API that feels native to React. It’s great for small to medium apps where you need shared state without heavy infrastructure. Its snapshot mechanism via useSnapshot makes integration with React straightforward, but it lacks advanced tooling compared to Redux.
Choose recoil when building a React application that requires scalable, atom-based state with strong support for asynchronous data and derived state. It’s ideal for medium to large apps where you want React-like composability without global store coupling. Note that development has slowed, and Meta no longer actively maintains it — consider this for existing projects or if its model fits perfectly.
Create the next immutable state tree by simply modifying the current tree
Winner of the "Breakthrough of the year" React open source award and "Most impactful contribution" JavaScript open source award in 2019
You can use Gitpod (a free online VSCode like IDE) for contributing online. With a single click it will launch a workspace and automatically:
yarn run start.so that you can start coding straight away.
The documentation of this package is hosted at https://immerjs.github.io/immer/
Did Immer make a difference to your project? Join the open collective at https://opencollective.com/immer!