formik vs recoil vs mobx vs mobx-state-tree vs react-query vs redux vs xstate vs zustand
State Management and Data Fetching Solutions for React Applications
formikrecoilmobxmobx-state-treereact-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
formik3,448,56534,381585 kB8375 months agoApache-2.0
recoil426,51519,5062.21 MB3223 years agoMIT
mobx028,1854.35 MB806 months agoMIT
mobx-state-tree07,0501.34 MB10210 days agoMIT
react-query049,0722.26 MB1543 years agoMIT
redux061,444290 kB412 years agoMIT
xstate029,4182.26 MB13412 days agoMIT
zustand057,68195 kB525 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: formik vs recoil vs mobx vs mobx-state-tree vs react-query vs redux vs xstate vs zustand

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

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

    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.

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

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

Formik.js

Build forms in React, without the tears.


Stable Release Blazing Fast gzip size license Discord

Visit https://formik.org to get started with Formik.

Organizations and projects using Formik

List of organizations and projects using Formik

Authors

Contributing

This monorepo uses yarn, so to start you'll need the package manager installed.

To run E2E tests you'll also need Playwright set up, which can be done locally via npx playwright install. Afterward, run yarn start:app and in a separate tab run yarn e2e:ui to boot up the test runner.

When you're done with your changes, we use changesets to manage release notes. Run yarn changeset to autogenerate notes to be appended to your pull request.

Thank you!

Contributors

Formik is made with <3 thanks to these wonderful people (emoji key):


Jared Palmer

💬 💻 🎨 📖 💡 🤔 👀 ⚠️

Ian White

💬 🐛 💻 📖 🤔 👀

Andrej Badin

💬 🐛 📖

Adam Howard

💬 🐛 🤔 👀

Vlad Shcherbin

💬 🐛 🤔

Brikou CARRE

🐛 📖

Sam Kvale

🐛 💻 ⚠️

Jon Tansey

🐛 💻

Tyler Martinez

🐛 📖

Tobias Lohse

🐛 💻

This project follows the all-contributors specification. Contributions of any kind welcome!

Related

  • TSDX - Zero-config CLI for TypeScript used by this repo. (Formik's Rollup configuration as a CLI)

Apache 2.0 License.