zustand vs redux vs xstate vs mobx vs react-query vs jotai vs recoil vs valtio
状态管理库
zustandreduxxstatemobxreact-queryjotairecoilvaltio类似的npm包:

状态管理库

状态管理库是用于管理应用程序状态的工具,特别是在复杂的用户界面中。它们帮助开发者在不同组件之间共享和管理状态,确保数据一致性和可预测性。这些库各有特点,适用于不同的场景和需求,选择合适的状态管理库可以显著提高开发效率和应用性能。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
zustand37,735,38958,86296 kB42 天前MIT
redux27,136,70261,446290 kB153 年前MIT
xstate3,016,15230,3482.31 MB1226 天前MIT
mobx2,018,95028,2124.88 MB213 天前MIT
react-query911,72650,4682.26 MB1654 年前MIT
jotai021,297113 kB224 天前MIT
recoil019,4102.21 MB3204 年前MIT
valtio010,243101 kB66 个月前MIT

功能对比: zustand vs redux vs xstate vs mobx vs react-query vs jotai vs recoil vs valtio

状态管理模式

  • zustand:

    Zustand 提供简单的 API,支持直接修改状态,适合小型项目的快速开发。

  • redux:

    Redux 使用集中式状态管理,通过 actions 和 reducers 管理状态变化,适合大型应用的可预测性和可维护性。

  • xstate:

    XState 使用状态机模型,明确状态转换和行为,适合需要复杂状态管理的应用。

  • mobx:

    MobX 使用可观察的状态和自动依赖追踪,允许状态变化自动更新相关组件,适合响应式编程。

  • react-query:

    React Query 专注于服务器状态管理,提供数据获取、缓存和同步功能,简化了与后端的交互。

  • jotai:

    Jotai 使用原子状态管理,每个状态都是独立的原子,允许局部更新,减少不必要的重渲染。

  • recoil:

    Recoil 允许创建原子状态和派生状态,支持复杂的状态逻辑和依赖关系,适合大型应用。

  • valtio:

    Valtio 使用代理模式,提供简单的状态管理,支持直接修改状态,适合快速开发。

学习曲线

  • zustand:

    Zustand 的学习曲线较低,API 简单易懂,适合快速开发。

  • redux:

    Redux 的学习曲线较陡,特别是对于初学者,需要理解 actions、reducers 和中间件的概念。

  • xstate:

    XState 的学习曲线较陡,需要理解状态机的概念和状态转换。

  • mobx:

    MobX 的学习曲线适中,提供了简单的 API,但需要理解可观察的概念。

  • react-query:

    React Query 的学习曲线较低,易于集成和使用,特别适合处理服务器数据。

  • jotai:

    Jotai 的学习曲线较低,易于上手,特别适合熟悉 React Hooks 的开发者。

  • recoil:

    Recoil 的学习曲线适中,特别适合已经熟悉 React 的开发者。

  • valtio:

    Valtio 的学习曲线较低,API 直观,适合快速上手。

性能

  • zustand:

    Zustand 的性能表现良好,简单的 API 和直接的状态管理使得更新高效。

  • redux:

    Redux 的性能在于其可预测性和调试工具,适合大型应用的状态管理。

  • xstate:

    XState 的性能依赖于状态机的设计,能够高效地管理复杂状态。

  • mobx:

    MobX 的性能依赖于自动依赖追踪,能够高效地更新相关组件,适合高频率的状态变化。

  • react-query:

    React Query 提供了高效的数据缓存和更新机制,能够显著提高数据获取性能。

  • jotai:

    Jotai 通过原子状态管理减少了不必要的重渲染,性能表现良好。

  • recoil:

    Recoil 的性能表现良好,特别是在处理复杂状态时,能够有效管理依赖关系。

  • valtio:

    Valtio 的性能良好,直接修改状态的方式使得状态更新高效。

扩展性

  • zustand:

    Zustand 的简单 API 使得扩展性良好,适合快速开发和小型项目。

  • redux:

    Redux 的中间件机制允许扩展功能,适合大型应用的需求。

  • xstate:

    XState 的状态机模型允许灵活扩展,适合复杂应用的需求。

  • mobx:

    MobX 提供了丰富的插件和扩展机制,适合复杂应用的需求。

  • react-query:

    React Query 提供了丰富的 API 和配置选项,支持自定义数据获取和缓存策略。

  • jotai:

    Jotai 允许通过原子组合和自定义 hooks 扩展功能,适合灵活的状态管理需求。

  • recoil:

    Recoil 的原子和派生状态机制使得扩展性良好,适合复杂的状态管理需求。

  • valtio:

    Valtio 的简单 API 使得扩展性良好,适合快速开发和迭代。

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

  • zustand:

    选择 Zustand 如果你需要一个简单且高效的状态管理库,特别是在小型或中型项目中。Zustand 提供了直观的 API 和良好的性能,适合快速开发。

  • redux:

    选择 Redux 如果你的应用需要集中式的状态管理,尤其是在大型应用中。Redux 提供了强大的中间件支持和调试工具,适合需要严格状态管理的场景。

  • xstate:

    选择 XState 如果你的应用需要复杂的状态机和状态管理。XState 提供了强大的状态机模型,适合需要明确状态转换和行为的应用。

  • mobx:

    选择 MobX 如果你偏好使用可观察的状态管理,并且需要自动化的依赖追踪。MobX 提供了简单的 API 和强大的性能,适合需要响应式编程的应用。

  • react-query:

    选择 React Query 如果你的应用需要处理服务器状态,特别是数据获取和缓存。它提供了强大的数据获取、缓存和更新功能,简化了与后端的交互。

  • jotai:

    选择 Jotai 如果你需要一个简单且灵活的状态管理解决方案,特别是在使用 React Hooks 的项目中。它的原子状态管理使得状态更新更为局部化,避免了不必要的重渲染。

  • recoil:

    选择 Recoil 如果你需要在 React 应用中实现更细粒度的状态管理。Recoil 允许你创建原子状态和派生状态,适合复杂的状态逻辑。

  • valtio:

    选择 Valtio 如果你想要一个轻量级且简单的状态管理库,支持代理模式。它的 API 直观,适合快速开发和小型项目。

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