jotai vs formik vs mobx vs react-query vs recoil vs redux vs xstate vs zustand
React 状态管理与数据流解决方案对比
jotaiformikmobxreact-queryrecoilreduxxstatezustand类似的npm包:

React 状态管理与数据流解决方案对比

formik、jotai、mobx、react-query、recoil、redux、xstate 和 zustand 都是 React 生态中用于处理状态、数据流或副作用的主流库,但各自解决的问题和适用场景有显著差异。formik 专注于表单状态与验证;react-query(现为 TanStack Query)专精于服务端状态(如 API 数据)的获取、缓存与同步;redux、zustand、jotai、recoil 和 mobx 主要用于管理客户端全局状态,但采用了不同的编程模型(如不可变 vs 可变、原子化 vs 单一 store);而 xstate 则通过有限状态机(FSM)和状态图来建模复杂交互逻辑。这些工具共同构成了现代 React 应用状态管理的完整工具链。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
jotai4,165,78421,300113 kB21 个月前MIT
formik2,724,83534,310585 kB8441 年前Apache-2.0
mobx028,2144.88 MB192 天前MIT
react-query050,4802.26 MB1694 年前MIT
recoil019,4112.21 MB3204 年前MIT
redux061,447290 kB153 年前MIT
xstate030,3572.31 MB1248 天前MIT
zustand058,87196 kB44 天前MIT

React 状态管理全景:从表单到状态机的深度对比

在现代 React 开发中,状态管理早已超越了简单的 useState。面对表单、服务端数据、全局 UI 状态乃至复杂交互逻辑,开发者需要一套组合拳。本文将深入剖析 formik、jotai、mobx、react-query、recoil、redux、xstate 和 zustand 这八大工具的核心机制、适用边界及真实代码实现。

📝 表单状态:Formik 的专属领域

formik 是唯一专注于表单的库,它通过封装表单的脏检查、验证、提交等逻辑,大幅减少重复代码。

// formik: 复杂表单管理
import { useFormik } from 'formik';
import * as Yup from 'yup';

const MyForm = () => {
  const formik = useFormik({
    initialValues: { email: '', password: '' },
    validationSchema: Yup.object({
      email: Yup.string().email().required(),
      password: Yup.string().min(8).required()
    }),
    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" disabled={!formik.isValid}>Submit</button>
    </form>
  );
};

其他库(如 zustand 或 redux)理论上也能管理表单状态,但缺乏内置的验证、提交处理和错误映射,需手动实现大量逻辑。

🌐 服务端状态:React Query 的统治区

react-query(现属 TanStack Query)专为服务端数据设计,自动处理缓存、去重、后台更新等难题。

// react-query: 服务端数据获取
import { useQuery, useMutation } from '@tanstack/react-query';

const UserProfile = ({ userId }) => {
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json())
  });

  const mutation = useMutation({
    mutationFn: (newData) => fetch(`/api/users/${userId}`, {
      method: 'PUT',
      body: JSON.stringify(newData)
    })
  });

  if (isLoading) return <div>Loading...</div>;
  return <div>{user.name}</div>;
};

注意:react-query 不管理 UI 本地状态。若需同时处理服务端数据和全局 UI 状态,应搭配 zustand 或 jotai 使用。

🧠 客户端状态:原子化 vs 单一 Store

原子化模型:Jotai 与 Recoil

jotai 和 recoil 将状态拆分为独立“原子”,组件只订阅所需部分,避免无效重渲染。

// jotai: 原子化状态
import { atom, useAtom } from 'jotai';

const countAtom = atom(0);
const doubleCountAtom = atom((get) => get(countAtom) * 2);

const Counter = () => {
  const [count, setCount] = useAtom(countAtom);
  const double = useAtom(doubleCountAtom)[0];
  return <div>{count} → {double}</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
});

const Counter = () => {
  const [count, setCount] = useRecoilState(countState);
  const double = useRecoilValue(doubleCountState);
  return <div>{count} → {double}</div>;
};

关键区别:jotai 无须唯一 key,API 更简洁;recoil 依赖 key 且支持更复杂的依赖图,但 Facebook 已将其移交社区维护。

单一 Store 模型:Redux 与 Zustand

redux 和 zustand 采用中心化 store,但实现哲学截然不同。

// redux: 经典 reducer 模式
import { createStore } from 'redux';

const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    default: return state;
  }
};

const store = createStore(reducer);

// 在组件中
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'increment' });
// zustand: 极简 hook 式 store
import { create } from 'zustand';

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 }))
}));

const Counter = () => {
  const { count, increment } = useStore();
  return <button onClick={increment}>{count}</button>;
};

zustand 无需 action/reducer,直接修改状态(内部仍保证不可变性),且天然支持异步操作,大幅降低使用门槛。

响应式可变模型:MobX

mobx 允许直接修改状态,通过装饰器自动追踪依赖。

// mobx: 可变响应式状态
import { makeAutoObservable } from 'mobx';
import { observer } from 'mobx-react-lite';

class CounterStore {
  count = 0;
  constructor() { makeAutoObservable(this); }
  increment = () => { this.count++; }
}

const counterStore = new CounterStore();

const Counter = observer(() => (
  <button onClick={counterStore.increment}>
    {counterStore.count}
  </button>
));

优势是代码直观,但可变性可能引发意外副作用,且调试时需依赖 MobX DevTools 查看依赖关系。

🤖 复杂逻辑:XState 的状态机范式

当交互逻辑涉及多个互斥状态(如“加载中”、“成功”、“失败”、“空状态”)时,xstate 通过状态图显式定义所有可能路径。

// 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: { on: { FETCH: 'loading' } },
    failure: { on: { FETCH: 'loading' } }
  }
});

const service = interpret(fetchMachine).start();
service.send('FETCH'); // 进入 loading 状态

配合 @xstate/react 的 useMachine hook,可直接在组件中使用:

import { useMachine } from '@xstate/react';

const FetchButton = () => {
  const [state, send] = useMachine(fetchMachine);
  
  if (state.matches('loading')) return <div>Loading...</div>;
  if (state.matches('success')) return <div>Success!</div>;
  
  return <button onClick={() => send('FETCH')}>Fetch</button>;
};

状态机强制开发者思考所有边界情况,但增加了初期设计成本,仅推荐用于高复杂度逻辑。

🔍 核心能力对比表

能力formikjotaimobxreact-queryrecoilreduxxstatezustand
表单管理✅❌❌❌❌❌❌❌
服务端状态❌❌❌✅❌❌❌❌
客户端状态❌✅✅❌✅✅⚠️*✅
原子化更新❌✅✅*N/A✅❌❌❌
中间件/插件❌有限✅✅有限✅✅有限
时间旅行调试❌❌✅❌✅✅✅❌
TypeScript 支持✅✅✅✅✅✅✅✅

*注:xstate 可管理客户端状态,但非其主要设计目标;mobx 的“原子化”指自动追踪依赖,非状态拆分。

💡 选型决策树

  1. 是否涉及表单? → 选 formik(复杂表单)或 react-hook-form(简单表单)。
  2. 是否调用 API 获取数据? → 必选 react-query(或 SWR)。
  3. 是否需要管理全局 UI 状态?
    • 追求极简:zustand
    • 需要细粒度更新:jotai(轻量)或 recoil(功能全)
    • 团队熟悉 OOP:mobx
    • 需要强约束和调试:redux
  4. 是否有复杂状态转换?(如多步骤流程、设备控制)→ 选 xstate。

🌟 组合使用最佳实践

现代应用常组合多个库:

  • react-query + zustand:服务端数据用 Query,UI 状态用 Zustand。
  • formik + react-query:表单提交触发 Query 的 mutation。
  • xstate + react-query:状态机控制查询流程(如重试逻辑)。
// 组合示例:Zustand (UI) + React Query (数据)
import { create } from 'zustand';
import { useQuery } from '@tanstack/react-query';

// UI 状态:模态框开关
const useUIStore = create((set) => ({
  isModalOpen: false,
  openModal: () => set({ isModalOpen: true })
}));

// 数据状态:用户列表
const useUsersQuery = () => useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(res => res.json())
});

const App = () => {
  const { isModalOpen, openModal } = useUIStore();
  const { data } = useUsersQuery();
  
  return (
    <div>
      <button onClick={openModal}>Open Modal</button>
      {isModalOpen && <Modal users={data} />}
    </div>
  );
};

📌 总结

  • 不要用一把锤子敲所有钉子:表单、服务端数据、UI 状态、复杂逻辑各有最优解。
  • 优先考虑问题域:先明确你要解决的是数据获取、表单验证还是状态流转,再选工具。
  • 组合优于替代:现代状态管理是分层的,react-query 几乎应成为任何数据驱动应用的标配,再根据 UI 状态复杂度选择客户端方案。

最终,没有“最好”的库,只有“最合适”当前场景的工具。理解每个库的设计哲学和边界,才能构建出既健壮又易维护的 React 应用。

如何选择: jotai vs formik vs mobx vs react-query vs recoil vs redux vs xstate vs zustand

  • jotai:

    选择 jotai 如果你偏好原子化(atomic)状态模型,希望避免 Redux 的样板代码,同时又不需要 Recoil 的复杂依赖图。它基于 React Context 但优化了渲染性能,支持异步原子和派生状态,适合中小型应用或作为 Recoil 的轻量替代方案。

  • formik:

    选择 formik 如果你正在构建包含复杂表单(如多步骤、动态字段、嵌套验证)的应用,并希望减少样板代码。它提供了一套完整的表单生命周期管理(包括值跟踪、错误处理、提交控制),特别适合需要与 Yup 等验证库集成的场景。但对于简单表单,React Hook Form 可能更轻量高效。

  • mobx:

    选择 mobx 如果你的团队熟悉面向对象编程,且希望以可变状态的方式编写响应式逻辑。它通过装饰器(或函数式 API)自动追踪依赖并更新组件,开发体验接近 Vue 的响应式系统。但需注意其运行时开销和调试复杂性,在大型团队中可能因隐式依赖导致维护困难。

  • react-query:

    选择 react-query(TanStack Query)如果你的应用重度依赖服务端数据(如 REST 或 GraphQL API)。它自动处理请求缓存、后台刷新、乐观更新、分页和无限滚动等常见需求,极大简化了数据获取逻辑。它不适用于 UI 本地状态管理,应与其他状态库配合使用。

  • recoil:

    选择 recoil 如果你需要细粒度的状态订阅和复杂的派生状态计算,且项目已深度集成 React Concurrent 模式。它的原子(atom)和选择器(selector)模型支持高效重渲染,但 API 相对复杂,学习曲线较陡,且 Facebook 已将其维护状态转为社区主导。

  • redux:

    选择 redux 如果你的应用需要严格的时间旅行调试、中间件生态(如 Redux Saga/Thunk)或强类型支持(配合 TypeScript)。尽管现代 React 提供了 Context + useReducer,Redux 仍因其成熟工具链(Redux DevTools)和可预测性在大型企业级项目中占有一席之地,但需权衡其样板代码成本。

  • xstate:

    选择 xstate 如果你的 UI 逻辑涉及复杂的状态转换(如向导流程、游戏逻辑、设备控制面板)。它通过可视化状态图强制显式定义所有可能的状态和事件,极大提升可维护性和可测试性。但对简单状态场景属于过度设计,且需要团队接受状态机思维模式。

  • zustand:

    选择 zustand 如果你追求极简 API 和零样板代码的全局状态管理。它基于单一 store 模型,但通过 hooks 避免了 Context 的 prop-drilling 问题,且天然支持 TypeScript。适合中小型项目快速上手,或作为 Redux 的现代化替代方案。

jotai的README


Jotai (light mode)


visit jotai.org or npm i jotai

Build Status Build Size Version Downloads Discord Shield Open Collective

Jotai scales from a simple useState replacement to an enterprise TypeScript application.

  • Minimal core API (2kb)
  • Many utilities and extensions
  • No string keys (compared to Recoil)

Examples: Demo 1 | Demo 2

First, create a primitive atom

An atom represents a piece of state. All you need is to specify an initial value, which can be primitive values like strings and numbers, objects, and arrays. You can create as many primitive atoms as you want.

import { atom } from 'jotai'

const countAtom = atom(0)
const countryAtom = atom('Japan')
const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])
const mangaAtom = atom({ 'Dragon Ball': 1984, 'One Piece': 1997, Naruto: 1999 })

Use the atom in your components

It can be used like React.useState:

import { useAtom } from 'jotai'

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  return (
    <h1>
      {count}
      <button onClick={() => setCount((c) => c + 1)}>one up</button>
      ...

Create derived atoms with computed values

A new read-only atom can be created from existing atoms by passing a read function as the first argument. get allows you to fetch the contextual value of any atom.

const doubledCountAtom = atom((get) => get(countAtom) * 2)

function DoubleCounter() {
  const [doubledCount] = useAtom(doubledCountAtom)
  return <h2>{doubledCount}</h2>
}

Creating an atom from multiple atoms

You can combine multiple atoms to create a derived atom.

const count1 = atom(1)
const count2 = atom(2)
const count3 = atom(3)

const sum = atom((get) => get(count1) + get(count2) + get(count3))

Or if you like fp patterns ...

const atoms = [count1, count2, count3, ...otherAtoms]
const sum = atom((get) => atoms.map(get).reduce((acc, count) => acc + count))

Derived async atoms needs suspense

You can make the read function an async function too.

const urlAtom = atom('https://json.host.com')
const fetchUrlAtom = atom(async (get) => {
  const response = await fetch(get(urlAtom))
  return await response.json()
})

function Status() {
  // Re-renders the component after urlAtom is changed and the async function above concludes
  const [json] = useAtom(fetchUrlAtom)
  ...

You can create a writable derived atom

Specify a write function at the second argument. get will return the current value of an atom. set will update the value of an atom.

const decrementCountAtom = atom(
  (get) => get(countAtom),
  (get, set, _arg) => set(countAtom, get(countAtom) - 1)
)

function Counter() {
  const [count, decrement] = useAtom(decrementCountAtom)
  return (
    <h1>
      {count}
      <button onClick={decrement}>Decrease</button>
      ...

Write only derived atoms

Just do not define a read function.

const multiplyCountAtom = atom(null, (get, set, by) =>
  set(countAtom, get(countAtom) * by),
)

function Controls() {
  const [, multiply] = useAtom(multiplyCountAtom)
  return <button onClick={() => multiply(3)}>triple</button>
}

Async actions

Just make the write function an async function and call set when you're ready.

const fetchCountAtom = atom(
  (get) => get(countAtom),
  async (_get, set, url) => {
    const response = await fetch(url)
    set(countAtom, (await response.json()).count)
  }
)

function Controls() {
  const [count, compute] = useAtom(fetchCountAtom)
  return (
    <button onClick={() => compute('http://count.host.com')}>compute</button>
    ...

Note about functional programming

Jotai's fluid interface is no accident — atoms are monads, just like promises! Monads are an established pattern for modular, pure, robust and understandable code which is optimized for change. Read more about Jotai and monads.

Links