最近研究了下 React 的状态管理库,当然,仅限在使用层面,也就是用着舒服的角度来选择到底使用哪个状态管理库。本着在 Github 上面看看 React 社区内状态管理库的流行程度和使用程度的层面,来进行选型,然后就有了这篇文章,关于我们最后选择用哪个,文章末尾告知。
选择库的原则如下:
- 全面拥抱 typescript,so 选择的库需要对 typescript 支持友好;
- react 自从 16.8 引入 hooks,接下来就开始了函数式编程的时代,所以不要有 class 这玩意;
- 一定要使用简单,不要太复杂,使用起来很轻量,重点是舒服;
- 支持模块化,可以集中管理,原子性低;
- 支持 esmodule,因为后面会考虑迁移到 vite,虽然现在还是 webpack。
截止目前为止,在 Github 上面看了一下当前比较流行的几个状态管理库的 star 数和 used by 的数量,以及 npm 上面的周下载量(weekly downloads),这可以从某些方面说明明该框架的受欢迎程度,也有很小的可能性不准确,不过很大程度上,对框架选型是有所帮助的。
库名 | github star | github used | npm 周下载量 |
---|---|---|---|
mobx | 23.9k | 83.9k | 671,787 |
redux-toolkit | 5.9k | 83.2k | 755,564 |
recoil | 13.5k | 83.9k | 95,245 |
zustand | 9.4k | 7.2k | 104,682 |
rematch | 7.3k | 2.5k | 33,810 |
concent | 950 | 65 | 1,263 |
上面表格中,就是我们接下来要进行挑选的对象,到底中意哪个,还得看看使用起来的时候的姿势,哪个更加舒服。
mobx
mobx 是一个非常优秀的 react 状态管理库,这毋容置疑,而且在 Github 上面,它的使用量也是做到了第一,官方文档地址zh.mobx.js。官网上面给的例子是非常简单的,大多数官网也都如此,可是我不需要简单的例子,我需要一个完整的项目的例子,于是参考了 github 上面的一个项目antd-pro-mobx。mobx 需要搭配 mobx-react 的连接库一起来使用。
按照 npm 上面的推荐是要使用 class + observe 函数包裹的方式,最新版本 v6:
import React from "react" import ReactDOM from "react-dom" import { makeAutoObservable } from "mobx" import { observer } from "mobx-react" // Model the application state. class Timer { secondsPassed = 0 constructor() { makeAutoObservable(this) } increase() { this.secondsPassed += 1 } reset() { this.secondsPassed = 0 } } const myTimer = new Timer() // Build a "user interface" that uses the observable state. const TimerView = observer(({ timer }) => ( <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button> )) ReactDOM.render(<TimerView timer={myTimer} />, document.body) // Update the 'Seconds passed: X' text every second. setInterval(() => { myTimer.increase() }, 1000)
新项目的从头开始,应该不会选择老版本的库区使用,一般会选择稳定的新版本的进行使用,关于 typescript 方面,看源码是已经在使用 typescript 来编写了,不过在官网和 npm 上面并没有看到 typescript 的蛛丝马迹,可能是还没发版吧。
我们对比我们的原则看下关于 mobx:
- 支持 typescript — NO
- 使用函数式编程 — NO
- 使用舒服 — OK
- 原子性低问题 — OK
- 支持 esmodule — OK
关于 mobx 部分就暂且到这,说的不对的地方欢迎告知,确实是才疏学浅,没怎么用过这么流行的状态管理库。
reduxjs/toolkit
toolkit,暂且这么叫吧,redux 官方状态管理库,cra 模板 redux(npx create-react-app --template redux
)自带状态管理库,cra 模板 redux-ts(npx create-react-app --template redux-typescript
)自带状态管理库.可能这两个模板也导致了 toolkit 的下载量和使用量非常大。也由于是 redux 官方库的原因,需要搭配 react-redux 来搭配使用。这些我们暂且不管,我们看下如何使用,亲测哈,如有使用不当,可以指出。
// index.ts 主入口文件 import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { RootStore } from 'modules/store'; import { fetchInfo } from 'modules/counter'; function App(props: any) { const count = useSelector((state: RootStore) => state.counter.value); const dispatch = useDispatch(); return ( <div> hello home <hr/> <button aria-label="Decrement value" onClick={() => dispatch(fetchInfo(2234))} > fetchInfo </button> <div> <span>{count}</span> </div> </div> ); }; ReactDOM.render( <App/>, document.getElementById('root'), );
上面是主入口文件的代码,可以看到这个使用方式还算是比较普遍,符合 redux 的使用方式。
// modules/store.ts import { configureStore, combineReducers } from '@reduxjs/toolkit'; import counter from './counter'; import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux'; const reducer = combineReducers({ counter, }); const store = configureStore({ reducer, }); export type RootStore = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppSelector: TypedUseSelectorHook<RootStore> = useSelector; export default store;
上面是 store 主文件的代码,这其实也是官方给出的合理使用方式。
// modules/counter.ts import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; const namespace: string = 'counter'; export interface ICounter { value: number; } const initialState: ICounter = { value: 1, }; // async 异步函数定义 export const fetchInfo = createAsyncThunk(`${namespace}/fetchInfo`, async (value: number) => { await sleep(1000); return { value: 9000 + value, }; }); // 创建带有命名空间的 reducer const counterSlice = createSlice({ name: namespace, initialState, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchInfo.pending, (state, action) => { state.value = 1000; }) .addCase(fetchInfo.fulfilled, (state, {payload}) => { state.value = payload.value; }) .addCase(fetchInfo.rejected, (state, {payload}) => { state.value = 500; }); }, }); const { reducer } = counterSlice; export default reducer;
上面是在实际 module 的使用,会产出一个 reducer,唯一不优雅的地方在于extraReducers
的调用方式采用了串联的方式,不过还可以通过对象的形式进行传递,不过在 ts 中支持不够友好,如下:
const counterSlice = createSlice({ name: namespace, initialState, reducers: {}, extraReducers: { [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => { state.value = 1000; }, [fetchInfo.pending.type]: (state: Draft<ICounter>, { payload }: PayloadAction<ICounter>) => { state.value = payload.value; }, [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => { state.value = 500; }, }, });
可以看到上面换成了对象的方式,不过在函数里面需要自己去写好类型声明;而串行的方式,typescript 已经自动推导出了函数所对应的参数类型。
我们对比我们的原则看下关于 toolkit:
- 支持 typescript — OK
- 使用函数式编程 — OK
- 使用舒服 — OK,除了 builder 的链式使用方式
- 原子性低问题 — OK
- 支持 esmodule — OK
recoil
recoil,react 官方状态管理库,随着 react17 而来,官方网址为recoiljs,其实透过官方文档,我们可以看到差不多是完全遵循了 react hooks 的使用方式,不需要搭配任何连接器,可以与 react 直接无缝连接。不过这其实也导致了原子性比较强,统一的状态管理需要对其进行二次封装,而且工作量不小。在 typescript 方面,0.3.0 开始支持,当前为止最新的版本是 0.3.1。例子我就看下官方的例子:
import React from 'react'; import { RecoilRoot, atom, selector, useRecoilState, useRecoilValue, } from 'recoil'; function App() { return ( <RecoilRoot> <CharacterCounter /> </RecoilRoot> ); } const textState = atom({ key: 'textState', // unique ID (with respect to other atoms/selectors) default: '', // default value (aka initial value) }); function CharacterCounter() { return ( <div> <TextInput /> <CharacterCount /> </div> ); } function TextInput() { const [text, setText] = useRecoilState(textState); const onChange = (event) => { setText(event.target.value); }; return ( <div> <input type="text" value={text} onChange={onChange} /> <br /> Echo: {text} </div> ); } const charCountState = selector({ key: 'charCountState', // unique ID (with respect to other atoms/selectors) get: ({get}) => { const text = get(textState); return text.length; }, }); function CharacterCount() { const count = useRecoilValue(charCountState); return <>Character Count: {count}</>; }
由上面,我们可以简单再对比下我们的原则:
- 支持 typescript — OK,虽然力度不是很大
- 使用函数式编程 — OK
- 使用舒服 — NO
- 原子性低问题 — NO
- 支持 esmodule — OK
zustand
zustand,这个库,说实话,是第一次看到,不过看了 npm 上面的例子,这个库还是很好用&很实用的,使用起来很舒服,提供的 api 不是很多,但是够精简,能够满足需求。没有单独的官网,不过 readme 写的足够详细,算是个地址吧npm zustand, 我们来看下官网提供的例子:
import React from "react"; import create from "zustand"; import PrismCode from "react-prism"; import "prismjs"; import "prismjs/components/prism-jsx.min"; import "prismjs/themes/prism-okaidia.css"; const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time)); const code = `import create from 'zustand' const useStore = create(set => ({ count: 1, inc: () => set(state => ({ count: state.count + 1 })), })) function Controls() { const inc = useStore(state => state.inc) return <button onClick={inc}>one up</button> ) function Counter() { const count = useStore(state => state.count) return <h1>{count}</h1> }`; const useStore = create((set) => ({ count: 1, inc: () => set((state) => ({ count: state.count + 1 })), sleep: async () => { await sleep(2000); set((state) => ({ count: state.count + 30 })); } })); function Counter() { const { count, inc, sleep } = useStore(); return ( <div > <span>{count}</span> <button onClick={inc}>one up</button> <button onClick={sleep}>30 up</button> </div> ); } export default function App() { return ( <div > <div > <div > <PrismCode className="language-jsx" children={code} /> <Counter /> </div> </div> </div> ); }
可以看到所有的数据使用的 createStore 进行包裹,里面可以定义任意类型,可以是 count 的这样的 stats 类型,也可以使用函数(包括异步函数),做到了最简单化;另外 zustand 还提供了一些其他的工具函数和中间件,关于中间件和工具函数等的如何使用,此处就不多说了,可以去 npm 看看.
由上面,我们可以简单再对比下我们的原则:
- 支持 typescript — OK, 但是官网描述里面的例子比较少
- 使用函数式编程 — OK
- 使用舒服 — OK
- 原子性低问题 — 不高不低,中等,可能需要使用到中间件来包裹,扩展使用
- 支持 esmodule — OK
rematch
rematch, 因为有部分项目在使用这个库,所以简单看了下使用。官网上面有很多例子,可以去看看: rematchjs. v1 的时候是不支持 typescript 的,使用上有两种方式(对于 effects):
// 方式一 effects: { fetchInfo: async () => { const res = await requestInfo(); this.setState({ ...res; }) } } // 方式二 effects: (dispatch) => { return { fetchInfo: async () => { const res = await requestInfo(); dispatch.info.setInfo(res); } } }
v2 的时候是增加了 typescript 的支持,不过却去掉了上面方式一的使用方式,只保留了第二种。具体例子可以前往rematch typescript 查看。这个使用方式其实与上面的 redux-toolkit 稍微有点相似,不过好像 rematch 最近下载量下降了不少。
rematch 在模块化封装部分做的很好,能够对所有的状态进行统一管理,然后可以按 models 进行划分功能,使用起来比较舒服。
由上面以及官网上面的一些例子,我们可以简单再对比下我们的原则:
- 支持 typescript — OK
- 使用函数式编程 — OK
- 使用舒服 — OK
- 原子性低问题 — OK
- 支持 esmodule — OK
concent
concent,另外一种状态管理器的实现方式,在使用上与 Vue3 的 setup 有很大相似之处。为什么会谈到 concent,因为有好几个项目在使用 concent,而且表现良好。官方网站concentjs.concent 功能非常强大,各种黑魔法,要想使用好 concent,会有比较大的学习成本,也就是使用起来可能不会很简单。
import { StrictMode } from "react"; import ReactDOM from "react-dom"; import { run, useConcent } from "concent"; const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time)); run({ counter: { state: { value: "" }, computed: {}, lifecycle: {}, reducer: { decrement: async (payload, moduleState) => { await sleep(1000); return { value: moduleState.value - 1 }; } } } }); const setup = (ctx) => { ctx.effect(() => { ctx.setState({ value: 1000 }); }, []); const increment = () => { console.log(1233); ctx.setState({ value: ctx.state.value + 1 }); }; return { increment }; }; export default function App() { const { state, settings, moduleReducer } = useConcent({ module: "counter", setup }); return ( <div className="App"> <h1>Hello Counter : {state.value}</h1> <div> <button onClick={settings.increment}>click increment</button> <button onClick={moduleReducer.decrement}>click decrement</button> </div> </div> ); } ReactDOM.render( <StrictMode> <App /> </StrictMode>, document.getElementById("root") );
完整的例子和 api 可以前往官网查看,支持 class 装饰器的模式,也支持函数 setup 的方式,也有不使用 setup 的方式,都能满足;问题就是 api 较多,学习成本较高。
根据经验和上面的例子,我们可以简单再对比下我们的原则:
- 支持 typescript — OK
- 使用函数式编程 — OK
- 使用舒服 — OK,就是学习成本较高
- 原子性低问题 — OK,支持模块化,状态统一管理
- 支持 esmodule — OK
结论
至此,这几种状态管理库的使用方式和原则对比差不多已经完成。可能有使用不当或者说明错误的地方,欢迎指出。
最终我们考虑了以下几点:
- 支持 typescript,使用函数式编程
- 支持模块化,状态统一管理
- 学习成本低,且状态管理库比较流行
- 考虑大家的意见,这很客观,选择一个大家都愿意使用的库
最终选择的是 redux 官方的 toolkit 来进行统一的状态管理。
本文结束,感谢阅读,欢迎讨论交流。如有不当之处感谢指出。