kea, mobx, react-redux, recoil, and redux are all libraries designed to manage application state in JavaScript applications, particularly those built with React. They provide mechanisms to store, update, and access shared data across components while maintaining predictable behavior and performance. redux is a standalone, framework-agnostic state container that enforces a unidirectional data flow using actions and reducers. react-redux is the official React binding for Redux, enabling efficient component integration. recoil is a state management library developed by Meta that uses atoms and selectors to model state as a directed graph. mobx takes a reactive programming approach, automatically tracking observable state changes and re-rendering affected components. kea builds on Redux concepts but offers a more declarative, logic-centric API with built-in side effect handling.
Managing state in React apps goes beyond useState. When you need shared, persistent, or complex state, choosing the right library affects code clarity, performance, and maintainability. Let’s compare these five approaches through real engineering lenses.
redux treats state as immutable snapshots. You dispatch plain objects (actions) that describe what happened, and pure functions (reducers) compute the next state. Everything flows one way: action → reducer → new state.
// redux: Action + Reducer
const increment = { type: 'counter/increment' };
function counterReducer(state = 0, action) {
if (action.type === 'counter/increment') {
return state + 1;
}
return state;
}
react-redux connects React components to a Redux store. It uses useSelector to read state and useDispatch to send actions.
// react-redux: Component binding
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.counter);
const dispatch = useDispatch();
return <button onClick={() => dispatch(increment)}>{count}</button>;
}
recoil models state as atoms (shared units) and selectors (derived/computed values). Components subscribe only to the atoms/selectors they use.
// recoil: Atom + Selector
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
const countAtom = atom({ key: 'count', default: 0 });
const doubledCount = selector({
key: 'doubledCount',
get: ({ get }) => get(countAtom) * 2
});
function Counter() {
const [count, setCount] = useRecoilState(countAtom);
const double = useRecoilValue(doubledCount);
return <div>{count} → {double}</div>;
}
mobx uses observable objects. You mark properties as observable, and any function reading them (like a React component) automatically re-runs when they change.
// mobx: Observable state
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';
class CounterStore {
count = 0;
constructor() { makeAutoObservable(this); }
increment() { this.count += 1; }
}
const store = new CounterStore();
const Counter = observer(() => (
<button onClick={() => store.increment()}>{store.count}</button>
));
kea wraps Redux patterns into logic objects. Each logic defines actions, reducers, and side effects in one place, then connects to components.
// kea: Logic object
import { kea } from 'kea';
const counterLogic = kea({
actions: { increment: true },
reducers: { count: [0, { increment: (state) => state + 1 }] },
});
function Counter() {
const { count } = counterLogic.useValues();
const { increment } = counterLogic.useActions();
return <button onClick={increment}>{count}</button>;
}
redux doesn’t handle async natively. You need middleware like redux-thunk or redux-saga.
// redux + thunk: Async action
const fetchUser = (id) => async (dispatch) => {
dispatch({ type: 'user/loading' });
const user = await api.getUser(id);
dispatch({ type: 'user/success', payload: user });
};
react-redux uses the same middleware — it just provides hooks to dispatch these thunks.
// react-redux: Dispatching thunk
const dispatch = useDispatch();
useEffect(() => { dispatch(fetchUser(123)); }, []);
recoil handles async in selectors using async/await. The framework manages loading/error states automatically.
// recoil: Async selector
const userQuery = selector({
key: 'userQuery',
get: async ({ get }) => {
const response = await fetch(`/api/user/${get(userIdAtom)}`);
return response.json();
}
});
// In component:
const user = useRecoilValue(userQuery); // Suspends on pending
mobx lets you write async logic directly in store methods. Use runInAction to batch updates after async calls.
// mobx: Async in store
fetchUser = async (id) => {
this.loading = true;
const user = await api.getUser(id);
runInAction(() => {
this.user = user;
this.loading = false;
});
};
kea includes built-in sagas/thunks. Define listeners that respond to actions.
// kea: Built-in side effects
const userLogic = kea({
actions: { loadUser: (id) => ({ id }), setUser: (user) => ({ user }) },
reducers: { user: [null, { setUser: (_, payload) => payload.user }] },
listeners: ({ actions }) => ({
loadUser: async ({ id }) => {
const user = await api.getUser(id);
actions.setUser(user);
}
})
});
redux / react-redux: By default, every dispatched action causes all useSelector calls to re-run. You must memoize selectors (createSelector) or use shallow equality checks to prevent unnecessary renders.
// react-redux: Memoized selector
const selectUser = createSelector(
[state => state.users],
users => users.find(u => u.id === 123)
);
const user = useSelector(selectUser); // Only changes if user object changes
recoil: Components re-render only when their specific atoms/selectors change. Fine-grained by design.
// recoil: Automatic granularity
const firstName = useRecoilValue(firstNameAtom); // Only updates if firstName changes
mobx: Reactivity is property-level. If a component reads store.user.name, it only re-renders when name changes — not when other parts of user update.
// mobx: Property-level tracking
const NameDisplay = observer(() => <span>{store.user.name}</span>);
// Won't re-render if store.user.email changes
kea: Uses Redux under the hood, so same granularity rules apply. But its useValues hook auto-memoizes based on the logic’s reducers.
// kea: Auto-memoized values
const { user } = userLogic.useValues(); // Only changes if user reducer updates
redux has unmatched tooling: Redux DevTools shows every action, lets you time-travel, and inspect state diffs. Middleware ecosystem is vast (persistence, logging, etc.).
react-redux inherits all Redux tooling plus React-specific optimizations (e.g., batch for multiple dispatches).
recoil offers Recoil DevTools (community-driven), but no official time-travel. Async handling is built-in, but SSR requires extra setup (<RecoilRoot> hydration).
mobx provides MobX Developer Tools to track observables and reactions. Debugging focuses on which observables triggered updates, not action history.
kea integrates with Redux DevTools out of the box. Its logic objects appear as named Redux slices, making debugging familiar to Redux users.
redux shines here: reducers are pure functions, easy to unit test. Actions are serializable, enabling replayable logs.
// redux: Testable reducer
test('counter increments', () => {
expect(counterReducer(0, increment)).toBe(1);
});
mobx tests require instantiating stores and checking observable state. Mutations are less explicit, so tests may be more brittle.
// mobx: Store testing
test('counter increments', () => {
const store = new CounterStore();
store.increment();
expect(store.count).toBe(1);
});
recoil, kea, and react-redux fall in between: recoil selectors are pure functions (testable), kea logic can be tested via its actions/reducers, and react-redux relies on Redux’s testability.
As of 2024:
redux, react-redux, and mobx are actively maintained with stable APIs.recoil is still used at Meta but has seen slower public development; no deprecation notice, but evaluate long-term support for enterprise use.kea is actively maintained (v3+ supports modern React) with regular releases.None of these packages are officially deprecated. All are viable for new projects, but consider team familiarity and project scale.
redux + react-reduxmobxrecoil (if comfortable with its maturity)keareact-redux| Feature | redux | react-redux | recoil | mobx | kea |
|---|---|---|---|---|---|
| State Model | Immutable | Immutable | Atoms | Observable | Redux-based |
| Async Handling | Middleware | Middleware | Selectors | Direct | Built-in |
| Granularity | Manual | Manual | Automatic | Property-level | Manual |
| DevTools | Excellent | Excellent | Basic | Good | Redux DevTools |
| Boilerplate | High | Medium | Low | Low | Medium |
| Learning Curve | Steep | Medium | Medium | Gentle | Medium |
There’s no “best” state manager — only what fits your team’s mental model and app’s needs. If you value explicitness and tooling, lean toward Redux variants. If you prefer writing natural mutations, MobX feels effortless. For new greenfield projects wanting fine-grained reactivity, Recoil or Kea offer compelling middle grounds. Always prototype with real use cases before committing.
Choose redux if you need a battle-tested, framework-agnostic state container with strong guarantees around predictability, middleware support, and developer tooling. It’s best paired with Redux Toolkit to reduce boilerplate. Ideal for complex applications requiring time-travel debugging, persistence, or strict separation of concerns. Avoid it for simple UIs where local component state suffices, or if your team prefers mutable state models.
Choose react-redux if you’re already using Redux or need tight integration with the Redux ecosystem (middleware like Redux Toolkit, Redux DevTools, time-travel debugging). It’s ideal for large-scale applications requiring strict unidirectional data flow, middleware extensibility, and strong community tooling. Avoid it if you’re building a small app where Redux’s boilerplate outweighs its benefits, or if you prefer more granular reactivity over global store subscriptions.
Choose mobx if you prioritize developer ergonomics and want to write state updates as direct mutations while still getting automatic reactivity. It shines in applications with complex, interdependent state where fine-grained reactivity reduces unnecessary re-renders. However, avoid it if your team requires strict immutability, auditability via action logs, or if you’re building in an environment where transparent reactivity could obscure data flow (e.g., large teams needing explicit traceability).
Choose recoil if you’re building a new React application and want fine-grained, asynchronous state dependencies with minimal boilerplate. Its atom-and-selector model works well for dynamic UIs with derived state and async queries. However, note that while actively used at Meta, its public roadmap has slowed; avoid it for mission-critical enterprise apps if long-term maintenance risk is a concern, and consider alternatives if you need server-side rendering compatibility out of the box.
Choose kea if you want Redux-like predictability and devtools support but prefer a more declarative, colocated logic pattern without writing boilerplate actions and reducers. It’s well-suited for medium-to-large apps where you need structured state management with built-in sagas or thunks, and you value colocating business logic with components. Avoid it if your team isn’t comfortable with Redux concepts or if you need maximum simplicity for small projects.
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.