@reduxjs/toolkit, @xstate/react, and mobx-react are three distinct approaches to managing application state in React. @reduxjs/toolkit is the official, opinionated package for using Redux with modern best practices, reducing boilerplate and simplifying store setup. @xstate/react provides React bindings for XState, a library for creating state machines and statecharts to model complex workflows and UI logic with explicit transitions. mobx-react connects React components to MobX observables, enabling automatic reactivity by tracking which data your components use and updating them when that data changes.
Choosing the right state management solution can make or break your React app’s maintainability, performance, and developer experience. While all three libraries — @reduxjs/toolkit, @xstate/react, and mobx-react — help manage state, they do so from fundamentally different angles. Let’s compare them through real-world engineering lenses.
@reduxjs/toolkit enforces a unidirectional data flow: state is read-only, and changes happen via dispatched actions processed by pure reducer functions. This makes every state transition explicit and testable.
// @reduxjs/toolkit: defineSlice creates actions + reducer
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Immer allows "mutating" syntax
}
}
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
@xstate/react models behavior as finite state machines. Your app can only be in one of several predefined states at a time, and transitions between them are triggered by events. This prevents impossible states by design.
// @xstate/react: state machine definition
import { createMachine } from 'xstate';
const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: { on: { TOGGLE: 'active' } },
active: { on: { TOGGLE: 'inactive' } }
}
});
mobx-react treats state as observable objects. Components automatically track which observables they read and re-render when those values change — no actions, no dispatchers, just plain assignments.
// mobx-react: observable class
import { makeAutoObservable } from 'mobx';
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count += 1; // Direct mutation
}
}
const store = new CounterStore();
How you change state reveals each library’s DNA.
With Redux Toolkit, you dispatch an action:
// Component using Redux Toolkit
import { useDispatch } from 'react-redux';
import { increment } from './counterSlice';
function Counter() {
const dispatch = useDispatch();
return <button onClick={() => dispatch(increment())}>+</button>;
}
With XState, you send an event to the service:
// Component using @xstate/react
import { useMachine } from '@xstate/react';
function Toggle() {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send('TOGGLE')}>
{state.value === 'active' ? 'ON' : 'OFF'}
</button>
);
}
With MobX, you call a method that mutates observable state:
// Component using mobx-react
import { observer } from 'mobx-react';
const Counter = observer(() => {
return <button onClick={() => store.increment()}>{store.count}</button>;
});
Async logic is handled very differently.
Redux Toolkit uses thunks (or RTK Query for data fetching):
// Async thunk in Redux Toolkit
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk('user/fetch', async (id) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
});
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false },
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.data = action.payload;
state.loading = false;
});
}
});
XState embeds side effects directly in state nodes using invoke or actions:
// XState with async service
const fetchUserMachine = createMachine({
id: 'fetchUser',
initial: 'idle',
context: { user: null },
states: {
idle: {
on: { FETCH: 'loading' }
},
loading: {
invoke: {
src: (ctx, evt) => fetch(`/api/users/${evt.id}`).then(res => res.json()),
onDone: { target: 'success', actions: assign({ user: (_, evt) => evt.data }) },
onError: 'failure'
}
},
success: {},
failure: {}
}
});
MobX uses flow or async methods that mutate observables:
// MobX async with flow (or just async/await)
import { flow } from 'mobx';
class UserStore {
user = null;
loading = false;
fetchUser = flow(function* (this: UserStore, id) {
this.loading = true;
try {
const res = yield fetch(`/api/users/${id}`);
this.user = yield res.json();
} finally {
this.loading = false;
}
});
}
Redux Toolkit requires manual memoization (useSelector with equality checks) to prevent unnecessary renders:
// Prevent extra renders in Redux
import { useSelector } from 'react-redux';
const selectCount = (state) => state.counter.value;
function CounterDisplay() {
const count = useSelector(selectCount); // Only re-renders if count changes
return <div>{count}</div>;
}
XState gives you stable state references — the state object from useMachine only changes when the actual state or context changes, so components naturally avoid extra renders.
// XState components re-render only on relevant changes
const [state] = useMachine(someMachine);
// Safe to use directly in render
MobX automatically tracks dependencies at the component level (when wrapped with observer), so only the exact pieces of state used trigger re-renders:
// MobX re-renders only when store.count changes
const Counter = observer(() => <div>{store.count}</div>);
Despite their differences, all three:
| Aspect | @reduxjs/toolkit | @xstate/react | mobx-react |
|---|---|---|---|
| State Model | Immutable, centralized store | Finite state machines | Observable mutable objects |
| Update Mechanism | Dispatch actions | Send events | Direct assignment |
| Reactivity | Manual selection | Automatic (stable refs) | Automatic (fine-grained) |
| Async Handling | Thunks / RTK Query | Invoke / services in machine | Async methods / flow |
| Learning Curve | Moderate (concepts to learn) | Steep (new mental model) | Gentle (feels like plain JS) |
| Best For | Large apps, traceability | Complex workflows | Rapid development, simplicity |
These aren’t just “state libraries” — they represent different philosophies about how your app should behave. Redux Toolkit asks you to declare every change. XState asks you to define every possible state. MobX asks you to just write code and trust it’ll work. Choose based on what kind of guarantees your project needs, not just what’s trendy.
Choose @reduxjs/toolkit if you need a predictable, centralized state container with strong developer tooling, time-travel debugging, and middleware support like Redux Thunk or RTK Query. It’s ideal for large teams or applications where explicit state updates and traceability are critical, and you’re comfortable with action-based state changes.
Choose @xstate/react when your UI or business logic involves complex, well-defined states and transitions—like wizards, form flows, or device control systems. It excels when you want to eliminate invalid states by design, visualize behavior as diagrams, and manage asynchronous side effects within a formal state machine model.
Choose mobx-react if you prefer writing code that feels like plain JavaScript objects and want components to automatically re-render when the data they use changes. It’s great for rapid prototyping or apps where performance isn’t bottlenecked by fine-grained reactivity, and you want to avoid action creators and reducers.
The official, opinionated, batteries-included toolset for efficient Redux development
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:
Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
The package includes a precompiled ESM build that can be used as a <script type="module"> tag directly in the browser.
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 core docs at https://redux.js.org includes the full Redux tutorials, as well usage guides on general Redux patterns.
The Redux Toolkit package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:
We can't solve every use case, but in the spirit of create-react-app, we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
Because of that, this package is deliberately limited in scope. It does not address concepts like "reusable encapsulated Redux modules", folder or file structures, managing entity relationships in the store, and so on.
Redux Toolkit also includes a powerful data fetching and caching capability that we've dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.
Redux Toolkit includes these APIs:
configureStore(): wraps createStore to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add whatever Redux middleware you supply, includes redux-thunk by default, and enables use of the Redux DevTools Extension.createReducer(): lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the immer library to let you write simpler immutable updates with normal mutative code, like state.todos[3].completed = true.createAction(): generates an action creator function for the given action type string. The function itself has toString() defined, so that it can be used in place of the type constant.createSlice(): combines createReducer() + createAction(). Accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.combineSlices(): combines multiple slices into a single reducer, and allows "lazy loading" of slices after initialisation.createListenerMiddleware(): lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. A lightweight alternative to Redux async middleware like sagas and observables.createAsyncThunk(): accepts an action type string and a function that returns a promise, and generates a thunk that dispatches pending/resolved/rejected action types based on that promisecreateEntityAdapter(): generates a set of reusable reducers and selectors to manage normalized data in the storecreateSelector() utility from the Reselect library, re-exported for ease of use.For details, see the Redux Toolkit API Reference section in the docs.
RTK Query is provided as an optional addon within the @reduxjs/toolkit package. It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer for your app. It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
RTK Query is built on top of the Redux Toolkit core for its implementation, using Redux internally for its architecture. Although knowledge of Redux and RTK are not required to use RTK Query, you should explore all of the additional global store management capabilities they provide, as well as installing the Redux DevTools browser extension, which works flawlessly with RTK Query to traverse and replay a timeline of your request & cache behavior.
RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:
import { createApi } from '@reduxjs/toolkit/query'
/* React-specific entry point that automatically generates
hooks corresponding to the defined endpoints */
import { createApi } from '@reduxjs/toolkit/query/react'
RTK Query includes these APIs:
createApi(): The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.fetchBaseQuery(): A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.<ApiProvider />: Can be used as a Provider if you do not already have a Redux store.setupListeners(): A utility used to enable refetchOnMount and refetchOnReconnect behaviors.See the RTK Query Overview page for more details on what RTK Query is, what problems it solves, and how to use it.
Please refer to our contributing guide to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Redux Toolkit.