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

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

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

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
zustand23,036,32757,14895 kB51 个月前MIT
formik3,668,95634,386585 kB8364 个月前Apache-2.0
react-query1,516,22148,6372.26 MB1483 年前MIT
jotai021,019517 kB38 天前MIT
mobx028,1824.35 MB865 个月前MIT
recoil019,5272.21 MB3223 年前MIT
redux061,437290 kB432 年前MIT
xstate029,2682.25 MB16314 天前MIT

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

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

📝 表单状态: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>
  );
};

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

🌐 服务端状态: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 状态,应搭配 zustandjotai 使用。

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

原子化模型:Jotai 与 Recoil

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

// 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

reduxzustand 采用中心化 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/reactuseMachine 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 应用。

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

  • zustand:

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

  • formik:

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

  • react-query:

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

  • jotai:

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

  • mobx:

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

  • recoil:

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

  • redux:

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

  • xstate:

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

zustand的README

Build Status Build Size Version Downloads Discord Shield

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy API based on hooks, isn't boilerplatey or opinionated.

Don't disregard it because it's cute. It has quite the claws, lots of time was spent dealing with common pitfalls, like the dreaded zombie child problem, react concurrency, and context loss between mixed renderers. It may be the one state-manager in the React space that gets all of these right.

You can try a live demo and read the docs.

npm install zustand

:warning: This readme is written for JavaScript users. If you are a TypeScript user, be sure to check out our TypeScript Usage section.

First create a store

Your store is a hook! You can put anything in it: primitives, objects, functions. State has to be updated immutably and the set function merges state to help it.

import { create } from 'zustand'

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

Then bind your components, and that's it!

Use the hook anywhere, no providers are needed. Select your state and the component will re-render on changes.

function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  return <button onClick={increasePopulation}>one up</button>
}

Why zustand over redux?

Why zustand over context?

  • Less boilerplate
  • Renders components only on changes
  • Centralized, action-based state management

Recipes

Fetching everything

You can, but bear in mind that it will cause the component to update on every state change!

const state = useBearStore()

Selecting multiple state slices

It detects changes with strict-equality (old === new) by default, this is efficient for atomic state picks.

const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)

If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can use useShallow to prevent unnecessary rerenders when the selector output does not change according to shallow equal.

import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

const useBearStore = create((set) => ({
  nuts: 0,
  honey: 0,
  treats: {},
  // ...
}))

// Object pick, re-renders the component when either state.nuts or state.honey change
const { nuts, honey } = useBearStore(
  useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
)

// Array pick, re-renders the component when either state.nuts or state.honey change
const [nuts, honey] = useBearStore(
  useShallow((state) => [state.nuts, state.honey]),
)

// Mapped picks, re-renders the component when state.treats changes in order, count or keys
const treats = useBearStore(useShallow((state) => Object.keys(state.treats)))

For more control over re-rendering, you may provide any custom equality function (this example requires the use of createWithEqualityFn).

const treats = useBearStore(
  (state) => state.treats,
  (oldTreats, newTreats) => compare(oldTreats, newTreats),
)

Overwriting state

The set function has a second argument, false by default. Instead of merging, it will replace the state model. Be careful not to wipe out parts you rely on, like actions.

const useFishStore = create((set) => ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () => set({}, true), // clears the entire store, actions included
  deleteTuna: () => set(({ tuna, ...rest }) => rest, true),
}))

Async actions

Just call set when you're ready, zustand doesn't care if your actions are async or not.

const useFishStore = create((set) => ({
  fishies: {},
  fetch: async (pond) => {
    const response = await fetch(pond)
    set({ fishies: await response.json() })
  },
}))

Read from state in actions

set allows fn-updates set(state => result), but you still have access to state outside of it through get.

const useSoundStore = create((set, get) => ({
  sound: 'grunt',
  action: () => {
    const sound = get().sound
    ...

Reading/writing state and reacting to changes outside of components

Sometimes you need to access state in a non-reactive way or act upon the store. For these cases, the resulting hook has utility functions attached to its prototype.

:warning: This technique is not recommended for adding state in React Server Components (typically in Next.js 13 and above). It can lead to unexpected bugs and privacy issues for your users. For more details, see #2200.

const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))

// Getting non-reactive fresh state
const paw = useDogStore.getState().paw
// Listening to all changes, fires synchronously on every change
const unsub1 = useDogStore.subscribe(console.log)
// Updating state, will trigger listeners
useDogStore.setState({ paw: false })
// Unsubscribe listeners
unsub1()

// You can of course use the hook as you always would
function Component() {
  const paw = useDogStore((state) => state.paw)
  ...

Using subscribe with selector

If you need to subscribe with a selector, subscribeWithSelector middleware will help.

With this middleware subscribe accepts an additional signature:

subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
import { subscribeWithSelector } from 'zustand/middleware'
const useDogStore = create(
  subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })),
)

// Listening to selected changes, in this case when "paw" changes
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log)
// Subscribe also exposes the previous value
const unsub3 = useDogStore.subscribe(
  (state) => state.paw,
  (paw, previousPaw) => console.log(paw, previousPaw),
)
// Subscribe also supports an optional equality function
const unsub4 = useDogStore.subscribe(
  (state) => [state.paw, state.fur],
  console.log,
  { equalityFn: shallow },
)
// Subscribe and fire immediately
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
  fireImmediately: true,
})

Using zustand without React

Zustand core can be imported and used without the React dependency. The only difference is that the create function does not return a hook, but the API utilities.

import { createStore } from 'zustand/vanilla'

const store = createStore((set) => ...)
const { getState, setState, subscribe, getInitialState } = store

export default store

You can use a vanilla store with useStore hook available since v4.

import { useStore } from 'zustand'
import { vanillaStore } from './vanillaStore'

const useBoundStore = (selector) => useStore(vanillaStore, selector)

:warning: Note that middlewares that modify set or get are not applied to getState and setState.

Transient updates (for often occurring state-changes)

The subscribe function allows components to bind to a state-portion without forcing re-render on changes. Best combine it with useEffect for automatic unsubscribe on unmount. This can make a drastic performance impact when you are allowed to mutate the view directly.

const useScratchStore = create((set) => ({ scratches: 0, ... }))

const Component = () => {
  // Fetch initial state
  const scratchRef = useRef(useScratchStore.getState().scratches)
  // Connect to the store on mount, disconnect on unmount, catch state-changes in a reference
  useEffect(() => useScratchStore.subscribe(
    state => (scratchRef.current = state.scratches)
  ), [])
  ...

Sick of reducers and changing nested states? Use Immer!

Reducing nested structures is tiresome. Have you tried immer?

import { produce } from 'immer'

const useLushStore = create((set) => ({
  lush: { forest: { contains: { a: 'bear' } } },
  clearForest: () =>
    set(
      produce((state) => {
        state.lush.forest.contains = null
      }),
    ),
}))

const clearForest = useLushStore((state) => state.clearForest)
clearForest()

Alternatively, there are some other solutions.

Persist middleware

You can persist your store's data using any kind of storage.

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

const useFishStore = create(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage', // name of the item in the storage (must be unique)
      storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
    },
  ),
)

See the full documentation for this middleware.

Immer middleware

Immer is available as middleware too.

import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'

const useBeeStore = create(
  immer((set) => ({
    bees: 0,
    addBees: (by) =>
      set((state) => {
        state.bees += by
      }),
  })),
)

Can't live without redux-like reducers and action types?

const types = { increase: 'INCREASE', decrease: 'DECREASE' }

const reducer = (state, { type, by = 1 }) => {
  switch (type) {
    case types.increase:
      return { grumpiness: state.grumpiness + by }
    case types.decrease:
      return { grumpiness: state.grumpiness - by }
  }
}

const useGrumpyStore = create((set) => ({
  grumpiness: 0,
  dispatch: (args) => set((state) => reducer(state, args)),
}))

const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })

Or, just use our redux-middleware. It wires up your main-reducer, sets the initial state, and adds a dispatch function to the state itself and the vanilla API.

import { redux } from 'zustand/middleware'

const useGrumpyStore = create(redux(reducer, initialState))

Redux devtools

Install the Redux DevTools Chrome extension to use the devtools middleware.

import { devtools } from 'zustand/middleware'

// Usage with a plain action store, it will log actions as "setState"
const usePlainStore = create(devtools((set) => ...))
// Usage with a redux store, it will log full action types
const useReduxStore = create(devtools(redux(reducer, initialState)))

One redux devtools connection for multiple stores

import { devtools } from 'zustand/middleware'

// Usage with a plain action store, it will log actions as "setState"
const usePlainStore1 = create(devtools((set) => ..., { name, store: storeName1 }))
const usePlainStore2 = create(devtools((set) => ..., { name, store: storeName2 }))
// Usage with a redux store, it will log full action types
const useReduxStore1 = create(devtools(redux(reducer, initialState)), { name, store: storeName3 })
const useReduxStore2 = create(devtools(redux(reducer, initialState)), { name, store: storeName4 })

Assigning different connection names will separate stores in redux devtools. This also helps group different stores into separate redux devtools connections.

devtools takes the store function as its first argument, optionally you can name the store or configure serialize options with a second argument.

Name store: devtools(..., {name: "MyStore"}), which will create a separate instance named "MyStore" in the devtools.

Serialize options: devtools(..., { serialize: { options: true } }).

Logging Actions

devtools will only log actions from each separated store unlike in a typical combined reducers redux store. See an approach to combining stores https://github.com/pmndrs/zustand/issues/163

You can log a specific action type for each set function by passing a third parameter:

const useBearStore = create(devtools((set) => ({
  ...
  eatFish: () => set(
    (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
    undefined,
    'bear/eatFish'
  ),
  ...

You can also log the action's type along with its payload:

  ...
  addFishes: (count) => set(
    (prev) => ({ fishes: prev.fishes + count }),
    undefined,
    { type: 'bear/addFishes', count, }
  ),
  ...

If an action type is not provided, it is defaulted to "anonymous". You can customize this default value by providing an anonymousActionType parameter:

devtools(..., { anonymousActionType: 'unknown', ... })

If you wish to disable devtools (on production for instance). You can customize this setting by providing the enabled parameter:

devtools(..., { enabled: false, ... })

React context

The store created with create doesn't require context providers. In some cases, you may want to use contexts for dependency injection or if you want to initialize your store with props from a component. Because the normal store is a hook, passing it as a normal context value may violate the rules of hooks.

The recommended method available since v4 is to use the vanilla store.

import { createContext, useContext } from 'react'
import { createStore, useStore } from 'zustand'

const store = createStore(...) // vanilla store without hooks

const StoreContext = createContext()

const App = () => (
  <StoreContext.Provider value={store}>
    ...
  </StoreContext.Provider>
)

const Component = () => {
  const store = useContext(StoreContext)
  const slice = useStore(store, selector)
  ...

TypeScript Usage

Basic typescript usage doesn't require anything special except for writing create<State>()(...) instead of create(...)...

import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import type {} from '@redux-devtools/extension' // required for devtools typing

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useBearStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        increase: (by) => set((state) => ({ bears: state.bears + by })),
      }),
      {
        name: 'bear-storage',
      },
    ),
  ),
)

A more detailed TypeScript guide is here and there.

Best practices

Third-Party Libraries

Some users may want to extend Zustand's feature set which can be done using third-party libraries made by the community. For information regarding third-party libraries with Zustand, visit the doc.

Comparison with other libraries