在本书,我们讨论了三个全局状态管理库:Zutand,Jotai 和 Valtio。让我们比较这三个库之间的共性与差异。这三个库,都有其独自的特性。
Zustand 在用法和 存储模型上,与 Redux类似。但是,Redux是 基于 reducers 函数来更新的。
Jotai 在 API 上 和 Recoil 类似,但Jotai的目的是提供一个 非基选择函数 渲染优化 的最小化 API。
Valtio 和 Mobx 在 更新方式上是一样的,都是可变式更新。但这两者的相似性就仅限于此了,它们实现 自动渲染优化的方式是不一样的。
三个库都提供了适合微状态管理的原始功能,但它们在编码风格和渲染优化方法上有所不同。
在本章,我们会通过比较这三个库的对标库的方式,讨论这三个库。之后,我们会讨论这三个库的共性与差异。我们会涵盖这几个主题:
- Zustand 与 Redux 的异同
- 理解何时使用 Jotai 和 Recoil
- 使用 Valito 和 MobX
- 比较 Zustand,Jotai 和 Valtio
Zustand 与 Redux 的异同
在一些情况下,Zustand 和 Redux 用起来是很相似的。这两者都是基于单项数据流的。在单向数据流中,我们要分发 代表 更新一个状态的指令 的 action函数,而当 状态被 action更新后,新的 状态会被 传导到需要用到它当 地方。把分发 action函数和 传导新状态分开后,数据的流转过程被简化了,整个系统也因此更具有可预测性。
另一方面,它们在更新方式上不同。Redux 的更新是 基于 reducer函数的。一个reducer函数,是以状态和action对象为参数的 纯函数,并返回 更新后的状态。尽管使用 reducer函数来更新要遵循更严格的规范,但这也 提升了 代码 的 可预测性。而Zustand则使用了更灵活的方式来更新状态,并不需要使用 reducer函数。
在这个 小节,我们会把一个 基于 Redux的例子 改造成以Zustand为状态管理库的例子,并在这个过程中比较这两者的不同。
Redux 和 Zustand 的例子
让我们看看 Redux 的官方 教程。这个教程也叫 基于 React-toolkit 的 现代 Redux。
我们可以用 来自 React Toolkit 的 configureStore
来创建一个 Redux 仓库:
js
// src/app/store.js
import { configureStore } from "@reudxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer
}
})
这个 configureStore
函数 以一个reducer 函数为参数,并返回一个 store变量。在这段代码中,它使用了一个 reducer 函数 - counterReducer
。
counterReducer
写在了一个独立的文件里。首先,我们要引入createSlice
,并定义initialState
:
js
// features/counter/counterSlice.js
import { createSlice } from "@reudxjs/toolkit";
const initialState = {
value: 0
}
之后,我们用 createSlice
和 initialState
来 定义 counterSlice
:
js
export const counterSlice = createSlice({
name: "Counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (
state,
action: PayloadAction<number>
) => {
state.value = action.payload;
}
}
})
变量 counterSlice 是使用 createSlice 函数创建的,它同时包含一个 reducer 和 actions。为了方便导入它们,我们提取了 reducer 和 action 属性并将它们分别导出:
js
export const {
increment,
decrement,
incrementByAmount
} = counterSlice.aciton;
export defalut counterSlice.reducer;
之后,我们要定义 使用 仓库的 Counter
组件。首先,我们要从 react-redux 引入两个 常用的钩子,并从 counterSlice 引入 两个 actions 函数。
js
// features/counter/Counter.jsx
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./counterSlice";
然后,就是ui逻辑了:
js
export function Counter() {
const count = useSelector((
state: { counter: { value: number } }
) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(increment())}>
Increment
</button>
<span>{count}</span>
<button onClick={() => dispatch(decrement())}>
decrement
</button>
</div>
)
}
这个组件,使用了来自 React Redux 的 useSelector
,useDispatch
函数。我们使用了 选择函数来 获得 仓库 内的值。注意,这个组件并没有直接从 仓库获取值。
最后,我们实现App
组件:
js
// App.jsx
import { Provider } from "react-redux";
import { store } from "./app/store";
import { Counter } from "./features/counter/Counter";
const App = () => {
<Provider store={store}>
<div>
<Counter/>
<Counter/>
</div>
</Provider>
}
export default App;
我们把 store
传给 Provider
组件。有了Provider
组件后,useSelector
钩子就可以在这个Context 内 获取 仓库的值了。
如下图所示,代码按照预期运行了。这两个Counter
组件共享同一个 count
变量:

然后,我们看看这个功能,如何用 Zustand实现。
首先,我们要从 Zustand 引入 create
函数:
js
// store.js
import create from "zustand";
之后,我们用TS定义 State
类型:
js
type State = {
counter: {
value: number
},
counterActions: {
increment: () => void;
decrement: () => void:
incrementByAmount: (amount: number) => void;
}
}
接下来,我们要定义一个store。在zustand,我们用 useStore
来代表一个 仓库:
js
export const useStore = create<State>((set) => ({
counter: { value: 0 },
counterActions: {
increment: () => {
set((state) => ({
counter: { value: state.counter.value + 1 }
}))
},
decrement: () => {
set((state) => ({
counter: { value: state.counter.value - 1 }
}))
},
incrementByAmount: (amount: number) => {
set((state) => ({
counter: { value: state.counter.value + amount }
}))
}
}
}))
这段代码定义了 counter 的状态 以及 counter 的 操作函数。
接下来,就是 实现 使用了 这个 仓库的 Counter
组件了:
js
// Counter.jsx
import { useStore } from "./store";
export function Counter() {
const count = useStore((state) => state.counter.value);
const { increment, decrement } = useStore(
(state) => state.counterActions
);
return (
<div>
<button onClick={increment}>
Increment
</button>
<span>{count}</span>
<button onClick={decrement}>
decrement
</button>
</div>
)
}
我们使用 useStore
来获取 counter 的值, 和 counter的 操作函数。需要注意的是,useStore
这个钩子是直接从 store 文件引入的
最后,我们可以定义 App
组件:
js
// App.jsx
import { Counter } from "./Counter";
const App = () => {
<div>
<Counter />
<Counter />
</div>
}
因为我们没有使用Context,所以这里不需要使用 Provider
组件。
接下来,我们要比较这两者的不同。
比较 Zustand 与 Redux
尽管刚刚的两个例子有很多共性,Zustand 和 Redux 还是有 很多 显著的 不同的:
-
这两个例子,最大的不同在于,文件结构。现代Redux 所推荐的是 features 目录结构,而
createSlice
方法也是为这个结构而设计的。这个模式对于 大型应用是非常有用的。至于 Zustand,是不在乎文件结构的,开发者可以 自行决定 文件目录结构。 -
第二不同是,是否采用了 Immer.js。Immer.js 允许开发者 以
state.value += 1
的风格来更新状态;现代Redux 是 默认 使用 Immer.js的。Zustand 本身 没有 默认使用 Immer.js, 我们的例子里, Zustand仓库也没有 使用 Immer.js。不过,也是可以在 Zustand 里 使用 Immer.js 的。 -
第三个不同,在于状态的传递。Redux 采用 Context 注入状态,而 Zustand 主要是 通过模块引入。Context 允许 状态 在运行时 被注入。Zustand 允许 开发者 通过 Context 来 注入 状态。
-
最重要的是,Redux toolkit 是 基于 支持单向数据流的 Redux。所以,在Redux 来更新值,需要通过 reducer 来 分发状态。这一 限制和规范,有时对于 代码的 可维护性 和 可拓展性是 很有帮助的。至于 Zustand,是 不关心 单向数据流的。
总结来说,现代 Redux更 关注 仓库的 文件结构,而 Zustand 则 没那么关注。本质上来说,Zustand 是 一个最小化的 状态管理库,而 Redux 背后 有更完整的 生态。虽然两者的用法相似,但其背后的 哲学是 非常不一样的。
在下一个小节, 我们要 比较 Recoil 和 Jotai。