recoil vs mobx-state-tree vs formik vs mobx vs react-query vs redux vs xstate vs zustand
State Management and Data Fetching Solutions for React Applications
recoilmobx-state-treeformikmobxreact-queryreduxxstatezustandSimilar 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
recoil459,99819,5292.21 MB3223 years agoMIT
mobx-state-tree132,0197,0551.29 MB105a year agoMIT
formik034,384585 kB8364 months agoApache-2.0
mobx028,1824.35 MB855 months agoMIT
react-query048,6552.26 MB1523 years agoMIT
redux061,438290 kB432 years agoMIT
xstate029,2762.25 MB16416 days agoMIT
zustand057,19095 kB4a month 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: recoil vs mobx-state-tree vs formik vs mobx vs react-query vs redux vs xstate vs zustand

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

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

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

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

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

  • 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 recoil

Recoil · NPM Version Node.js CI GitHub license Follow on Twitter

Recoil is an experimental state management framework for React.

Website: https://recoiljs.org

Documentation

Documentation: https://recoiljs.org/docs/introduction/core-concepts

API Reference: https://recoiljs.org/docs/api-reference/core/RecoilRoot

Tutorials: https://recoiljs.org/resources

Installation

The Recoil package lives in npm. Please see the installation guide

To install the latest stable version, run the following command:

npm install recoil

Or if you're using yarn:

yarn add recoil

Or if you're using bower:

bower install --save recoil

Contributing

Development of Recoil happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Recoil.

License

Recoil is MIT licensed.