redux vs formik vs xstate vs mobx vs react-query vs recoil vs mobx-state-tree vs zustand
State Management and Data Fetching Solutions for React Applications
reduxformikxstatemobxreact-queryrecoilmobx-state-treezustandSimilar Packages:
State Management and Data Fetching Solutions for React Applications

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.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
redux21,080,03861,450290 kB422 years agoMIT
formik3,498,60834,382585 kB8363 months agoApache-2.0
xstate3,376,00929,2202.22 MB16810 days agoMIT
mobx2,745,05428,1704.35 MB834 months agoMIT
react-query1,434,49148,4082.26 MB1483 years agoMIT
recoil464,24919,5372.21 MB3233 years agoMIT
mobx-state-tree110,8907,0581.29 MB105a year agoMIT
zustand056,87795 kB35 days agoMIT

State Management and Data Fetching in React: A Practical Guide

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.

📥 Server Data vs Client State: The Fundamental Split

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.

🧮 Form State: Local and Ephemeral

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.

🔁 Reactive Client State: Mutable vs Immutable

Mutable with Automatic Tracking

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)
});

Immutable with Explicit Updates

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>;
}

🤖 Complex UI Logic: State Machines

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).

🔄 Combining Libraries: Best Practices

  • Never store server data in Redux/Zustand — use react-query instead.
  • Use Formik for complex forms, but keep form state local unless sharing across routes.
  • Pair XState with any state lib — it handles workflow logic, not data storage.
  • MobX vs Redux: Choose based on team preference for mutable (MobX) vs immutable (Redux) styles.

🛠️ Real-World Scenarios

Scenario 1: Dashboard with Live Data

  • Server data: react-query for charts, user info, notifications.
  • Client state: zustand for theme, sidebar open/closed.
  • Why? Separation of concerns; no cache invalidation bugs.

Scenario 2: Multi-Step Booking Flow

  • Workflow: xstate for step navigation (e.g., dates → guests → payment).
  • Form state: formik for each step’s inputs.
  • Server data: react-query for availability checks.
  • Why? XState ensures users can’t skip steps; Formik handles validation per step.

Scenario 3: Collaborative Document Editor

  • Local state: mobx for cursor positions, undo history (mutable, reactive).
  • Server sync: react-query for document fetch/save, with WebSockets for live updates.
  • Why? MobX’s fine-grained reactivity minimizes re-renders during typing.

📊 Summary Table

LibraryBest ForMental ModelKey Strength
formikComplex form stateLocal, ephemeralValidation, submission handling
mobxReactive client stateMutableAutomatic reactivity
mobx-state-treeStructured reactive stateMutable + typedSnapshots, patches, devtools
react-queryServer data fetchingCache-centricBackground sync, mutations
recoilFine-grained React stateAtom/selectorConcurrent mode ready
reduxPredictable global stateImmutableDevtools, middleware ecosystem
xstateComplex UI workflowsStatechartsPrevents invalid states
zustandSimple global stateHook-basedMinimal boilerplate

💡 Final Advice

  • Start with React’s built-in state (useState, useReducer) for local component state.
  • Add react-query as soon as you fetch data from an API.
  • Reach for zustand or recoil when you need simple global state.
  • Use redux only if you need its devtools or middleware (e.g., logging, undo).
  • Pick mobx if your team prefers mutable code and automatic reactivity.
  • Choose xstate for anything with multi-step logic or guarded transitions.
  • Reserve 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.

How to Choose: redux vs formik vs xstate vs mobx vs react-query vs recoil vs mobx-state-tree vs zustand
  • redux:

    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.

  • formik:

    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.

  • xstate:

    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.

  • mobx:

    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.

  • react-query:

    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.

  • recoil:

    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.

  • mobx-state-tree:

    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.

  • zustand:

    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.

README for redux

Redux Logo

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.

GitHub Workflow Status npm version npm downloads redux channel on discord

Installation

Create a React Redux App

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.

Documentation

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.

Learn Redux

Redux Essentials Tutorial

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.

Redux Fundamentals Tutorial

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.

Help and Discussion

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!

Before Proceeding Further

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:

  • You have reasonable amounts of data changing over time
  • You need a single source of truth for your state
  • You find that keeping all your state in a top-level component is no longer sufficient

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:

Basic Example

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.

Logo

You can find the official logo on GitHub.

Change Log

This project adheres to Semantic Versioning. Every release, along with the migration instructions, is documented on the GitHub Releases page.

License

MIT