formik, mobx, mobx-state-tree, react-query, recoil, redux, xstate, and zustand are libraries that address different aspects of state management and data synchronization in React applications. While redux, recoil, zustand, mobx, and mobx-state-tree focus primarily on managing client-side application state, react-query specializes in server-state management — handling data fetching, caching, synchronization, and background updates. formik is a form-handling library that manages local form state, validation, and submission. xstate provides a finite state machine and statechart implementation for modeling complex UI logic with predictable transitions. These tools vary significantly in mental model, API design, and scope, making them suitable for different architectural needs.
Managing state in React apps isn’t one-size-fits-all. Some libraries handle form inputs, others manage server data, and some orchestrate complex UI workflows. Let’s cut through the noise and compare how each tool solves real problems.
First, separate concerns: server state (data from APIs) and client state (UI state, form inputs, local preferences).
react-query is purpose-built for server state. It caches responses, auto-refetches when components mount, and handles mutations safely.
// react-query: Fetch and cache user data
import { useQuery, useMutation } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId)
});
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => queryClient.invalidateQueries(['user', userId])
});
if (isLoading) return <div>Loading...</div>;
return <div>{user.name}</div>;
}
All other libraries (redux, zustand, etc.) manage client state. Mixing server data into them leads to cache invalidation headaches — let react-query handle that.
formik treats forms as isolated units. It tracks values, errors, touched fields, and submission state without polluting global stores.
// formik: Manage a login form
import { useFormik } from 'formik';
function LoginForm() {
const formik = useFormik({
initialValues: { email: '', password: '' },
validate: (values) => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
},
onSubmit: (values) => login(values)
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
onChange={formik.handleChange}
value={formik.values.email}
/>
{formik.errors.email && <div>{formik.errors.email}</div>}
<button type="submit">Login</button>
</form>
);
}
For simple forms, React’s useState often suffices. For complex nested forms with dynamic fields, Formik’s structure pays off.
mobx lets you write mutable code that React re-renders automatically when observables change.
// mobx: Observable store
import { makeAutoObservable } from 'mobx';
class CounterStore {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++; // Mutate directly
}
}
const store = new CounterStore();
// In component
import { observer } from 'mobx-react-lite';
const Counter = observer(() => <div>{store.count}</div>);
mobx-state-tree adds structure to MobX with models, types, and actions:
// mobx-state-tree: Typed state tree
import { types } from 'mobx-state-tree';
const Todo = types.model({
id: types.identifier,
title: types.string,
done: false
}).actions(self => ({
toggle() {
self.done = !self.done; // Still mutable
}
}));
const Store = types.model({
todos: types.array(Todo)
});
redux enforces immutability via reducers:
// redux: Slice with Redux Toolkit
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
incremented: (state) => {
state.value += 1; // Draft state is mutable, but result is immutable
}
}
});
// In component
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return <button onClick={() => dispatch(counterSlice.actions.incremented())}>{count}</button>;
}
zustand offers a middle ground — a mutable store internally, but accessed via hooks:
// zustand: Simple store
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 }))
}));
function Counter() {
const { count, increment } = useStore();
return <button onClick={increment}>{count}</button>;
}
recoil uses atoms (state units) and selectors (derived state):
// recoil: Atoms and selectors
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>;
}
xstate models behavior as statecharts, preventing invalid transitions:
// xstate: Light switch machine
import { createMachine, interpret } from 'xstate';
const lightSwitchMachine = createMachine({
id: 'light',
initial: 'off',
states: {
off: { on: { TOGGLE: 'on' } },
on: { on: { TOGGLE: 'off' } }
}
});
// In component
import { useMachine } from '@xstate/react';
function LightSwitch() {
const [state, send] = useMachine(lightSwitchMachine);
return (
<button onClick={() => send('TOGGLE')}>
Light is {state.value}
</button>
);
}
This prevents bugs like "turning on an already-on light" in complex flows (e.g., checkout wizards).
react-query instead.react-query for charts, user info, notifications.zustand for theme, sidebar open/closed.xstate for step navigation (e.g., dates → guests → payment).formik for each step’s inputs.react-query for availability checks.mobx for cursor positions, undo history (mutable, reactive).react-query for document fetch/save, with WebSockets for live updates.| Library | Best For | Mental Model | Key Strength |
|---|---|---|---|
formik | Complex form state | Local, ephemeral | Validation, submission handling |
mobx | Reactive client state | Mutable | Automatic reactivity |
mobx-state-tree | Structured reactive state | Mutable + typed | Snapshots, patches, devtools |
react-query | Server data fetching | Cache-centric | Background sync, mutations |
recoil | Fine-grained React state | Atom/selector | Concurrent mode ready |
redux | Predictable global state | Immutable | Devtools, middleware ecosystem |
xstate | Complex UI workflows | Statecharts | Prevents invalid states |
zustand | Simple global state | Hook-based | Minimal boilerplate |
useState, useReducer) for local component state.react-query as soon as you fetch data from an API.zustand or recoil when you need simple global state.redux only if you need its devtools or middleware (e.g., logging, undo).mobx if your team prefers mutable code and automatic reactivity.xstate for anything with multi-step logic or guarded transitions.formik for forms too complex for basic hooks.The right tool depends on your app’s shape — not trends. Keep server and client state separate, and avoid over-engineering early on.
Choose redux when you need a predictable, centralized store with strict unidirectional data flow, extensive devtooling (like time-travel debugging), and a vast middleware ecosystem (e.g., Redux Toolkit, RTK Query). It’s ideal for large teams that benefit from enforced structure and traceability. Avoid it for small apps or when server-state dominates — use react-query for data fetching and consider lighter alternatives like Zustand for client state.
Choose formik when you need a battle-tested, dedicated solution for managing complex forms with validation, submission handling, and field-level state. It integrates well with any state management system and avoids the boilerplate of manually wiring up onChange handlers. However, for simple forms or when using modern React patterns like hooks extensively, consider whether a lighter approach (e.g., react-hook-form) might suffice.
Choose xstate when your UI logic involves complex workflows, multi-step processes, or guarded transitions that are hard to model with boolean flags or enums. Its visualizable statecharts prevent invalid states and make behavior explicit. It pairs well with any React state solution but shines in domains like wizards, modals, drag-and-drop, or device control. Don’t use it for simple CRUD apps where basic state suffices.
Choose mobx when you prefer a mutable, reactive programming model that feels natural and imperative. It’s ideal for teams comfortable with class-based or observable objects and who want automatic reactivity without manual selectors or reducers. Use it when you need fine-grained updates and don’t want to structure your state around immutability. Avoid if your team strongly prefers functional, immutable paradigms.
Choose react-query whenever your app fetches data from servers. It handles caching, background refetching, stale-while-revalidate strategies, pagination, mutations, and error retrying out of the box. It eliminates the need to store server data in global client state and keeps your UI synchronized with the backend automatically. Don’t use it for purely client-side state — pair it with another library like Zustand or Redux for that.
Choose recoil if you’re building a React app and want a state management system deeply integrated with React’s concurrent rendering features. Its atom-and-selector model allows fine-grained reactivity and derived state with minimal re-renders. It’s well-suited for apps with complex interdependent state that benefits from memoization and async selectors. However, its ecosystem is less mature than Redux’s, and migration from other systems may require significant refactoring.
Choose mobx-state-tree when you want the reactivity of MobX combined with strong runtime type safety, structured state trees, and built-in support for snapshots, patches, and time-travel debugging. It enforces a more disciplined architecture than plain MobX and is excellent for medium-to-large applications requiring predictable state evolution and serialization. It adds complexity, so avoid for small apps where simpler solutions suffice.
Choose zustand when you want a lightweight, hook-based global state solution with minimal boilerplate and no context providers. It uses a mutable store internally but exposes an immutable-like API via hooks, offering great performance and simplicity. It’s perfect for most medium-sized apps needing shared state without Redux’s ceremony. Avoid if you require advanced devtools or strict immutability guarantees.
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.