mobx, recoil, and redux are state management libraries for JavaScript applications, primarily used with React. They help manage shared, persistent state across components while providing mechanisms for updates, side effects, and performance optimization. mobx uses observable objects and automatic dependency tracking, recoil models state as fine-grained atoms and derived selectors, and redux enforces a single immutable store updated through pure reducer functions triggered by dispatched actions.
Choosing a state management solution is one of the most consequential decisions you’ll make when building a React application. mobx, recoil, and redux each offer distinct approaches to handling shared state, with trade-offs in complexity, performance, reactivity model, and developer experience. Let’s break down how they work under real-world conditions.
mobx treats state as observable objects that you mutate directly. Changes automatically trigger updates in components that depend on that state.
// mobx: observable state with direct mutation
import { makeAutoObservable } from 'mobx';
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++; // Direct mutation
}
}
const store = new CounterStore();
recoil models state as atoms (individual units) and selectors (derived state). You never mutate atoms directly — you replace their entire value.
// recoil: atomic state with immutable updates
import { atom, useSetRecoilState } from 'recoil';
const countState = atom({
key: 'count',
default: 0
});
function Counter() {
const setCount = useSetRecoilState(countState);
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
redux enforces a single, immutable store updated exclusively through pure functions called reducers triggered by actions.
// redux: single store, actions, and reducers
const increment = () => ({ type: 'INCREMENT' });
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
default:
return state;
}
};
mobx uses transparent reactivity: it tracks which observables your component reads during rendering and automatically re-renders when those values change. No manual subscription needed.
// mobx: auto-tracking via observer
import { observer } from 'mobx-react-lite';
const CounterDisplay = observer(({ store }) => (
<div>{store.count}</div> // Automatically tracked
));
recoil requires explicit hooks (useRecoilValue, useRecoilState) to subscribe to atoms or selectors. Reactivity is opt-in per component.
// recoil: explicit subscription
import { useRecoilValue } from 'recoil';
function CounterDisplay() {
const count = useRecoilValue(countState); // Explicit subscription
return <div>{count}</div>;
}
redux traditionally required manual subscription via connect() or useSelector. Modern Redux Toolkit simplifies this, but you still explicitly declare what slice of state you need.
// redux: explicit selector
import { useSelector } from 'react-redux';
function CounterDisplay() {
const count = useSelector(state => state.counter.count);
return <div>{count}</div>;
}
mobx encourages domain-driven stores — you create classes or objects that encapsulate related state and logic. This scales well for complex domains but can lead to scattered state if not organized carefully.
// mobx: multiple stores
const userStore = new UserStore();
const cartStore = new CartStore();
// Passed via context or modules
recoil promotes fine-grained atoms, even for small pieces of state. This avoids unnecessary re-renders but can result in many atom declarations.
// recoil: many small atoms
const firstNameState = atom({ key: 'firstName', default: '' });
const lastNameState = atom({ key: 'lastName', default: '' });
// Or combine into one object atom if preferred
redux mandates a single state tree. All state lives in one big object, usually normalized. This provides a clear source of truth but can feel rigid for loosely coupled features.
// redux: single state tree
{
users: { /* normalized */ },
posts: { /* normalized */ },
ui: { loading: false }
}
mobx lets you write async methods directly in stores using async/await. No middleware needed.
// mobx: async in store
fetchUser = async (id) => {
this.loading = true;
try {
this.user = await api.getUser(id);
} finally {
this.loading = false;
}
};
recoil uses asynchronous selectors or atom effects for side effects. Selectors can return promises, and components will render pending states automatically.
// recoil: async selector
const userQuery = selector({
key: 'userQuery',
get: async ({ get }) => {
const id = get(userIdState);
return await api.getUser(id);
}
});
redux requires middleware like redux-thunk or redux-saga to handle async logic. Thunks are simple; sagas offer powerful control flow but add complexity.
// redux: thunk
const fetchUser = (id) => async (dispatch) => {
dispatch({ type: 'FETCH_START' });
const user = await api.getUser(id);
dispatch({ type: 'FETCH_SUCCESS', payload: user });
};
mobx uses the observer higher-order component (or hook equivalent) to wrap components. It integrates via React context for store injection.
// mobx: provider and observer
import { Provider, observer } from 'mobx-react';
<Provider store={store}>
<App />
</Provider>
const App = observer(() => <CounterDisplay />);
recoil requires a RecoilRoot at the top of your app. Components use hooks directly — no wrappers needed beyond the root.
// recoil: RecoilRoot
import { RecoilRoot } from 'recoil';
<RecoilRoot>
<App />
</RecoilRoot>
redux needs a Provider wrapping your app, passing the store. Components use hooks like useSelector and useDispatch.
// redux: Provider
import { Provider } from 'react-redux';
<Provider store={store}>
<App />
</Provider>
mobx feels the most “magical” — you write plain JavaScript classes and mutations, and reactivity just works. But this can obscure data flow for newcomers.
recoil strikes a balance: minimal boilerplate, explicit subscriptions, and built-in async support. However, its API is less mature and adoption is narrower.
redux demands discipline: actions, reducers, selectors, middleware. But this structure pays off in large teams with strict debugging and time-travel requirements.
| Aspect | mobx | recoil | redux |
|---|---|---|---|
| Learning Curve | Moderate (concepts: observables) | Low–Moderate (atoms/selectors) | Steep (actions, reducers, middleware) |
| Boilerplate | Low | Very Low | High |
| Debugging | Good (MobX DevTools) | Limited (basic React DevTools) | Excellent (Redux DevTools) |
| Performance | Excellent (fine-grained updates) | Excellent (per-atom subscriptions) | Good (requires memoization/selectors) |
| Async Support | Built-in (async methods) | Built-in (async selectors) | Requires middleware |
| Team Scalability | Medium (mutation-based logic) | Medium (newer, less ecosystem) | High (strict unidirectional flow) |
Choose mobx if you want to model state as mutable objects with automatic reactivity, prefer OOP-style stores, and value writing business logic close to your data. Ideal for medium-to-large apps where developer velocity matters more than strict functional purity.
Choose recoil if you’re starting a new React app and want fine-grained, hook-based state with minimal setup and built-in async capabilities. Best for teams comfortable with React hooks and willing to adopt a newer, less battle-tested library.
Choose redux if you need predictable state transitions, time-travel debugging, strong middleware ecosystem (logging, persistence, etc.), or are working in a large team that benefits from enforced structure. Still the gold standard for complex, long-lived applications where traceability is critical.
All three libraries solve real problems. mobx embraces mutability with smart tracking, recoil brings granular reactivity to the React hooks era, and redux offers unmatched predictability through immutability and unidirectional flow. Your choice should reflect your team’s tolerance for magic vs. discipline, your app’s complexity, and how much you value debuggability over convenience.
Choose redux if you need strict predictability, time-travel debugging, a rich middleware ecosystem (like Redux Toolkit, RTK Query, or sagas), or are working in a large team that benefits from enforced unidirectional data flow. It remains the strongest choice for complex, long-term applications where traceability, testing, and state serialization are critical.
Choose mobx if you prefer modeling state as mutable objects with automatic reactivity, want to colocate business logic with data in domain-driven stores, and value writing straightforward imperative code without boilerplate. It’s well-suited for medium to large applications where developer experience and rapid iteration are priorities, and you’re comfortable with mutation-based state updates.
Choose recoil if you’re building a new React application and want a lightweight, hooks-native solution with fine-grained reactivity, built-in async support via selectors, and minimal setup. It works best when your team embraces React’s modern patterns and doesn’t require extensive devtooling or a mature ecosystem.
Redux is a predictable state container for JavaScript apps.
It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. On top of that, it provides a great developer experience, such as live code editing combined with a time traveling debugger.
You can use Redux together with React, or with any other view library. The Redux core is tiny (2kB, including dependencies), and has a rich ecosystem of addons.
Redux Toolkit is our official recommended approach for writing Redux logic. It wraps around the Redux core, and contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
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:
npm install @reduxjs/toolkit react-redux
For the Redux core library by itself:
npm install redux
For more details, see the Installation docs page.
The Redux core docs are located at https://redux.js.org, and include the full Redux tutorials, as well usage guides on general Redux patterns:
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 Essentials tutorial is a "top-down" tutorial that teaches "how to use Redux the right way", using our latest recommended APIs and best practices. We recommend starting there.
The Redux Fundamentals tutorial is a "bottom-up" tutorial that teaches "how Redux works" from first principles and without any abstractions, and why standard Redux usage patterns exist.
The #redux channel of the Reactiflux Discord community is our official resource for all questions related to learning and using Redux. Reactiflux is a great place to hang out, ask questions, and learn - please come and join us there!
Redux is a valuable tool for organizing your state, but you should also consider whether it's appropriate for your situation. Please don't use Redux just because someone said you should - instead, please take some time to understand the potential benefits and tradeoffs of using it.
Here are some suggestions on when it makes sense to use Redux:
Yes, these guidelines are subjective and vague, but this is for a good reason. The point at which you should integrate Redux into your application is different for every user and different for every application.
For more thoughts on how Redux is meant to be used, please see:
The whole global state of your app is stored in an object tree inside a single store. The only way to change the state tree is to create an action, an object describing what happened, and dispatch it to the store. To specify how state gets updated in response to an action, you write pure reducer functions that calculate a new state based on the old state and the action.
Redux Toolkit simplifies the process of writing Redux logic and setting up the store. With Redux Toolkit, the basic app logic looks like:
import { createSlice, configureStore } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
incremented: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
},
decremented: state => {
state.value -= 1
}
}
})
export const { incremented, decremented } = counterSlice.actions
const store = configureStore({
reducer: counterSlice.reducer
})
// Can still subscribe to the store
store.subscribe(() => console.log(store.getState()))
// Still pass action objects to `dispatch`, but they're created for us
store.dispatch(incremented())
// {value: 1}
store.dispatch(incremented())
// {value: 2}
store.dispatch(decremented())
// {value: 1}
Redux Toolkit allows us to write shorter logic that's easier to read, while still following the original core Redux behavior and data flow.
You can find the official logo on GitHub.
This project adheres to Semantic Versioning. Every release, along with the migration instructions, is documented on the GitHub Releases page.