formik, jotai, mobx, react-query, recoil, redux, xstate, and zustand are libraries that address different aspects of state management and data synchronization in React applications. While formik specializes in form state and validation, react-query focuses on server-state management including caching, background updates, and request deduplication. The remaining libraries — jotai, mobx, recoil, redux, xstate, and zustand — provide varying approaches to managing client-side application state, ranging from global stores (redux, zustand) to fine-grained reactivity (mobx, jotai, recoil) and explicit state machines (xstate). Each library makes different trade-offs between simplicity, scalability, performance, and conceptual model.
Managing state in React apps isn’t one-size-fits-all. Some libraries handle form inputs, others sync with servers, and the rest organize your app’s internal data. Let’s compare these eight tools by how they solve real problems.
formik exists solely for forms. It tracks values, errors, touched fields, and submission state automatically.
// Formik
import { useFormik } from 'formik';
function MyForm() {
const formik = useFormik({
initialValues: { email: '' },
validate: (values) => {
const errors = {};
if (!values.email) errors.email = 'Required';
return errors;
},
onSubmit: (values) => alert(JSON.stringify(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">Submit</button>
</form>
);
}
Other libraries like zustand or redux can store form data, but they don’t handle validation, dirty checks, or submission orchestration. For anything beyond trivial forms, formik (or alternatives like react-hook-form) saves significant effort.
react-query treats server data as a first-class citizen. It caches responses, deduplicates requests, and manages background updates.
// React Query
import { useQuery, useMutation } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const mutation = useMutation({
mutationFn: updateUser,
onSuccess: () => {
// Invalidate and refetch user data
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
if (isLoading) return <div>Loading...</div>;
return <div>{data.name}</div>;
}
None of the other libraries (jotai, mobx, etc.) handle HTTP caching, retries, or stale-while-revalidate strategies. They’re for client state only. Always pair react-query (or similar) with a client-state library — they solve complementary problems.
mobx)MobX lets you write mutable code that “just works” thanks to proxies and observables.
// MobX
import { makeAutoObservable } from 'mobx';
class Timer {
secondsPassed = 0;
constructor() {
makeAutoObservable(this);
}
increase() {
this.secondsPassed += 1;
}
}
const timer = new Timer();
// In component
import { observer } from 'mobx-react-lite';
const TimerView = observer(() => <div>{timer.secondsPassed}</div>);
Changes to secondsPassed automatically re-render TimerView. No reducers, no dispatches.
redux, zustand)Both enforce immutability but differ in API surface.
// Redux (with Redux Toolkit)
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Draft state is mutable
},
},
});
// In component
import { useSelector, useDispatch } from 'react-redux';
function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return <button onClick={() => dispatch(increment())}>{count}</button>;
}
// Zustand
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>;
}
Zustand skips providers and connects directly to components. Redux offers stronger devtooling and middleware.
jotai, recoil)Both use atoms (units of state) and selectors (derived state).
// Jotai
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
const doubleCount = useAtomValue(doubleCountAtom);
return <div>{doubleCount}</div>;
}
// Recoil
import { atom, selector, useRecoilState, useRecoilValue } from 'recoil';
const countState = atom({ key: 'count', default: 0 });
const doubleCountState = selector({
key: 'doubleCount',
get: ({ get }) => get(countState) * 2,
});
function Counter() {
const [count, setCount] = useRecoilState(countState);
const doubleCount = useRecoilValue(doubleCountState);
return <div>{doubleCount}</div>;
}
Jotai doesn’t require string keys, avoids provider trees, and has a smaller API. Recoil’s string keys enable devtools but add verbosity.
xstate)When logic has clear states and transitions (e.g., idle → loading → success/error), XState prevents invalid states.
// XState
import { createMachine, interpret } from 'xstate';
const fetchMachine = createMachine({
id: 'fetch',
initial: 'idle',
states: {
idle: { on: { FETCH: 'loading' } },
loading: {
on: { RESOLVE: 'success', REJECT: 'failure' },
},
success: { type: 'final' },
failure: { on: { RETRY: 'loading' } },
},
});
const service = interpret(fetchMachine).start();
// In component
import { useMachine } from '@xstate/react';
function FetchButton() {
const [state, send] = useMachine(fetchMachine);
if (state.matches('idle')) {
return <button onClick={() => send('FETCH')}>Fetch</button>;
}
if (state.matches('loading')) return <div>Loading...</div>;
if (state.matches('success')) return <div>Success!</div>;
return <button onClick={() => send('RETRY')}>Retry</button>;
}
This guarantees you can’t show a “Retry” button in the “idle” state — impossible with boolean flags.
zustand, jotai, and recoil avoid this by letting components subscribe to specific state slices.mobx uses fine-grained reactivity: only components using a changed observable re-render.redux requires useSelector with proper memoization (or RTK’s auto-memoized selectors) to prevent unnecessary renders.react-query’s caching means components sharing the same query key automatically stay in sync without extra wiring.redux has the richest middleware ecosystem (thunks, sagas, observables) and devtools for time-travel debugging.zustand supports middleware for persistence, logging, and immutability checks via simple function composition.mobx integrates with React DevTools and offers utilities for serializing state.react-query includes built-in support for pagination, infinite queries, and mutations with rollback.As of 2024:
recoil is in maintenance mode per Meta’s official statement. New projects should evaluate jotai as a more actively developed alternative with similar concepts.formik, jotai, mobx, react-query, redux, xstate, zustand) are actively maintained with regular releases.It’s common — and encouraged — to use multiple libraries together:
react-query + zustand: Use React Query for server data, Zustand for UI state (e.g., sidebar open/closed).xstate + mobx: Use XState for workflow logic, MobX for the underlying data models.formik + redux: Formik handles form state, Redux stores the final submitted data globally.Avoid using two general-purpose state managers (e.g., Redux + Zustand) unless you have a very specific reason — it adds complexity without benefit.
| Library | Best For | State Model | Learning Curve | Key Strength |
|---|---|---|---|---|
formik | Complex forms | Form-specific | Low | Validation, submission handling |
jotai | Global/client state | Atomic | Low | Minimal, no providers, async atoms |
mobx | Object-oriented state | Observable | Medium | Transparent reactivity |
react-query | Server data | Cache-centric | Low | Automatic caching, background updates |
recoil | Fine-grained reactivity | Atomic | Medium | Selectors, concurrent mode ready |
redux | Large-scale apps | Centralized | Medium-High | Devtools, middleware, ecosystem |
xstate | Stateful workflows | State machine | High | Prevents invalid states |
zustand | Simple global state | Hook-based | Low | Lightweight, no boilerplate |
react-query for any data from APIs — it’s transformative for async logic.zustand.jotai (or mobx if you like mutability).xstate only when your logic has clear states/transitions — don’t force it.formik for forms too complex for basic useState.recoil for new projects due to reduced maintenance; jotai is a drop-in conceptual replacement.The right tool depends on your problem’s shape — not hype. Match the library to the task, and don’t hesitate to combine them when needed.
Choose redux when you need a predictable, centralized store with strong devtooling, middleware support (like Redux Toolkit’s RTK Query), and a mature ecosystem. Modern Redux with Redux Toolkit eliminates much of the historical boilerplate and is well-suited for large applications requiring strict state traceability, time-travel debugging, or complex side-effect handling via middleware. Avoid it for small apps where its structure adds unnecessary overhead.
Choose zustand for a lightweight, hook-based global state solution that avoids common pitfalls like context re-renders. It’s ideal for most medium-sized apps needing shared state without Redux’s ceremony, offering direct store access, middleware support, and easy persistence. Its simplicity and performance make it a strong default choice for client state when you don’t need fine-grained reactivity or state machine semantics.
Choose formik when you need a battle-tested, dedicated solution for managing complex forms with validation, submission handling, and field-level state. It’s ideal for large forms with nested structures, dynamic fields, or intricate validation logic, especially when you want to avoid manually wiring up onChange handlers and error states. However, for simple forms or when using modern React patterns like controlled components with hooks, lighter alternatives may suffice.
Choose xstate when your UI or business logic is best modeled as a finite state machine or statechart — for example, multi-step wizards, complex animations, or workflows with clear transitions between states. It enforces explicit state definitions and prevents invalid transitions, making logic easier to test and visualize. Pair it with other state libraries for data storage, as it focuses on control flow rather than data persistence.
Choose jotai when you want a minimal, atomic state model that scales from simple local state to complex global state without boilerplate. Its atoms compose naturally, support async and derived values out of the box, and integrate seamlessly with React’s concurrent features. It’s particularly well-suited for teams that prefer a functional, immutable approach but want better performance than context-based solutions through automatic render optimization.
Choose mobx when you prefer an object-oriented, mutable state model with transparent reactivity. It excels in scenarios where you have deeply nested or frequently updated state and want automatic dependency tracking without manual selectors or memoization. Use it if your team is comfortable with classes and decorators (though modern MobX supports plain objects) and values writing code that closely mirrors how data is used in views.
Choose react-query whenever your app fetches data from APIs. It handles caching, background refetching, mutations, pagination, and optimistic updates automatically, reducing the need for manual loading/error states and cache invalidation logic. It’s not a general-purpose state manager — pair it with another library like zustand or redux for client-only state, but let it manage all server-derived data.
Choose recoil if you want fine-grained reactivity with a declarative, atom-and-selector model that integrates tightly with React’s rendering lifecycle. It’s powerful for complex UIs where many components depend on overlapping subsets of state, as it minimizes unnecessary re-renders. However, note that Facebook has deprioritized its development; while stable, new projects might consider alternatives like jotai with similar ergonomics and active maintenance.
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.