mobx-react, redux, and unstated represent three distinct philosophies for managing state in React applications. redux enforces a strict, unidirectional data flow with a single immutable store, relying on pure functions (reducers) and explicit actions to ensure predictability. mobx-react leverages observable data structures and automatic dependency tracking to update components reactively, mimicking standard object-oriented patterns. unstated was an early experiment that utilized React's Context API to share state without external dependencies, though it has since been superseded by native React features and other modern solutions.
Managing state in React applications often boils down to a trade-off between strict control and developer convenience. redux, mobx-react, and unstated each tackle this problem with fundamentally different mental models. While redux treats state as an immutable log of events, mobx-react treats it as live, observable data. unstated attempted to simplify things by leaning on React's own context system but has since become obsolete. Let's break down how they handle real-world engineering challenges.
redux requires you to describe what happened via an action object, then process that action in a pure reducer function to return a new state object. You cannot change the state directly.
// redux: Dispatch an action
store.dispatch({ type: 'INCREMENT', payload: 1 });
// redux: Pure reducer function
function counterReducer(state = 0, action) {
if (action.type === 'INCREMENT') {
return state + action.payload;
}
return state;
}
mobx-react allows you to modify state directly on observable objects. The library intercepts these changes and notifies subscribers automatically.
// mobx-react: Direct mutation
import { makeAutoObservable } from "mobx";
class Counter {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++; // Direct mutation triggers updates
}
}
unstated relied on a Container class where you defined methods to update internal state, similar to setState in class components.
// unstated: Container method update
import { Container } from 'unstated';
class CounterContainer extends Container {
state = { count: 0 };
increment = () => {
this.setState({ count: this.state.count + 1 });
};
}
redux connects components using the connect Higher-Order Component (HOC) or the useSelector hook. You must explicitly map the specific slice of state you need.
// redux: Using hooks
import { useSelector } from 'react-redux';
function CounterDisplay() {
const count = useSelector((state) => state.counter);
return <div>{count}</div>;
}
mobx-react uses the observer wrapper. It automatically tracks which observables are read during render and re-renders the component only when those specific values change.
// mobx-react: Observer wrapper
import { observer } from 'mobx-react';
const CounterDisplay = observer(({ store }) => {
return <div>{store.count}</div>; // Auto-tracks 'count'
});
unstated used a Subscribe component or a useContainer hook (in later versions) to inject the container instance into the component.
// unstated: Subscribe component
import { Subscribe } from 'unstated';
import { CounterContainer } from './containers';
function CounterDisplay() {
return (
<Subscribe to={[CounterContainer]}>
{(counter) => <div>{counter.state.count}</div>}
</Subscribe>
);
}
redux handles async logic (like API calls) via middleware such as redux-thunk or redux-saga. Actions can return functions instead of plain objects.
// redux: Redux Thunk
const fetchData = () => async (dispatch) => {
dispatch({ type: 'LOADING' });
const response = await api.getData();
dispatch({ type: 'SUCCESS', payload: response });
};
mobx-react often handles side effects inside actions or uses reaction / when utilities to respond to state changes automatically.
// mobx-react: Action with async logic
import { makeAutoObservable } from "mobx";
class Store {
data = null;
constructor() {
makeAutoObservable(this);
}
async fetchData() {
const response = await api.getData();
this.data = response; // Updates UI automatically
}
}
unstated typically pushed side effect logic directly into the container methods, relying on the component to call these methods after user events.
// unstated: Container method with async logic
class DataContainer extends Container {
state = { data: null };
loadData = async () => {
const response = await api.getData();
this.setState({ data: response });
};
}
It is critical to note that unstated is deprecated. The official repository and npm page indicate that the project is no longer maintained. The patterns it introduced were proven viable, and React now includes these capabilities natively. Using unstated in new projects introduces unnecessary risk and technical debt. Developers should migrate existing unstated implementations to React Context with useReducer or adopt modern, lightweight libraries.
You are building a complex financial dashboard with hundreds of interacting widgets, strict audit requirements, and a large team of developers.
reduxYou are building a tool where users drag nodes, edit properties in real-time, and see immediate graph updates.
mobx-reactYou need to pass the current user or theme setting down to deeply nested components without prop drilling.
unstated)// Modern React: Native Context replacement
const UserContext = React.createContext();
function App() {
const [user, setUser] = React.useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
<Dashboard />
</UserContext.Provider>
);
}
| Feature | redux | mobx-react | unstated |
|---|---|---|---|
| Core Philosophy | Immutable State + Explicit Actions | Observable State + Direct Mutation | Context-Based Containers |
| Boilerplate | High (Actions, Reducers, Store) | Low (Classes + Decorators/Hooks) | Minimal |
| Learning Curve | Steep (Functional concepts) | Moderate (OOP concepts) | Low (But deprecated) |
| Reactivity | Manual (Selectors) | Automatic (Fine-grained) | Manual (Context Subscribe) |
| Status | ✅ Active & Standard | ✅ Active & Popular | ❌ Deprecated |
redux remains the industry standard for applications where predictability and debuggability are paramount. Its ecosystem is vast, and its patterns are well-understood by most senior developers. It shines when the cost of a bug is high and the state logic is complex.
mobx-react offers a compelling alternative for teams that find Redux too verbose or restrictive. By embracing mutability and observables, it reduces code volume and often improves performance in highly interactive apps through automatic fine-grained updates. It feels more like writing standard JavaScript classes.
unstated served an important historical role by proving that React Context could manage state effectively. However, its job is now done. React itself has evolved to include the features unstated provided, making the library redundant. Do not start new projects with unstated. Instead, use native React Context for simple needs or redux/mobx for complex global state.
Choose redux when you need a highly predictable state container with a strict audit trail, making it easier to debug complex state transitions and implement features like time-travel debugging. It is the standard choice for large-scale enterprise applications where many developers collaborate, as the rigid architecture enforces consistency and makes data flow explicit. Select this if your application requires robust middleware ecosystems for handling side effects, logging, or persistence in a standardized way.
Choose mobx-react if your team prefers an object-oriented style where state mutations feel natural and you want to minimize boilerplate code. It excels in complex domains with many interdependent data points, as it automatically tracks dependencies and updates only the specific components that need re-rendering. This approach is ideal for rapid development cycles where developer velocity and intuitive code structure are prioritized over strict immutability constraints.
Do NOT choose unstated for new projects, as it is deprecated and no longer maintained. Its core functionality has been fully absorbed into React itself via the Context API and hooks like useReducer and useContext. If you previously relied on unstated for simple state sharing, you should migrate to native React Context or consider lightweight modern alternatives like Zustand or Jotai that offer better performance and API ergonomics.
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.