react状态管理调研
背景
在全民hooks的大背景下,dva虽然很🐮👃,但是也许可能maybe或者不再是一个最好的选择🙏,但直接使用hooks进行状态管理,可能在复杂应用中会掉入深坑(我暂时还没掉过,其他大哥文章里说的),而且刚好团队也需要进行一个状态管路库的调研。
统计
大致就统计了这些主流的。
名称 | github地址 | star | 是否调研 | 备注 |
---|---|---|---|---|
rxjs | github.com/ReactiveX/r… | 27.6 | N | 老哥们&&学习成本太大 |
mobx | github.com/mobxjs/mobx | 25.6 | N | 老哥们&&大家都会? |
zustand | github.com/pmndrs/zust… | 21 | Y | |
recoil | github.com/facebookexp… | 17.5 | Y | 源码太多了,没看 |
dva | github.com/dvajs/dva | 16.1 | N | 大家全会 |
jotai | github.com/pmndrs/jota… | 9.8 | Y | |
redux-toolkit | github.com/reduxjs/red… | 8.4 | N | 不想写redux |
rematch | github.com/rematch/rem… | 8.1 | N | 写法思路和dva一样 |
valtio | github.com/pmndrs/valt… | 5.2 | Y | |
hox | github.com/umijs/hox | 1.3 | Y | 看了源码,暂时有点迷惑。一般来说都是存一个状态,hox把hook当作状态存起来,然后再当作一个容器,hook内部维护的状态(useState)作为状态输出。说的不对别喷我。 |
那么多库,听过的没听过的加起来十几个,但是看了看最后想要调研的那几个,
呵呵,虚假的繁荣罢了。
需要了解的背景知识
在了解这些库之前,需要先了解一下 react18 中新提供的api useSyncExternalStore,它用来帮助库的开发和维护者更好地管理外部状态。
正常情况下,我们写一个最简单的外部状态管理hook,是这样的,包含状态的获取、修改还有外部的订阅,是不是很熟悉,和redux 差不多。(代码都是视频里抄的,建议直接看视频)
import { useCallback, useEffect, useState } from 'react';
const createStore = initialState => {
let state = initialState;
const listeners = new Set();
const getState = () => state;
const setState = fn => {
state = fn(state);
listeners.forEach(l => l());
}
const subscribe = listener => {
listeners.add(listener);
return () => listeners.delete(listener);
}
return { getState, setState, subscribe };
}
const useStore = (store, selector) => {
const [state, setState] = useState(() => selector(store.getState()));
useEffect(() => {
const callback = () => setState(selector(store.getState()));
const unsubscribe = store.subscribe(callback);
callback();
return unsubscribe;
}, [store, selector]);
return state;
}
然后,创建一个外部状态,并且在组件里消费外部状态。
const store = createStore({ count: 0, text: 'hello' });
const Counter = () => {
const count = useStore(
store,
useCallback(state => state.count, [])
);
const inc = () => {
store.setState(state => ({
...state,
count: state.count + 1
}))
}
return (
<div>
{count}
<button onClick={inc}>+1</button>
</div>
)
}
const TextBox = () => {
const text = useStore(
store,
useCallback(state => state.text, [])
);
const setText = e => {
store.setState(state => ({
...state,
text: e.target.value
}))
}
return (
<div>
<input type="text" value={text} onChange={setText}/>
</div>
)
}
function App() {
return (
<div className="App">
<Counter/>
<Counter/>
<TextBox/>
<TextBox/>
</div>
);
}
大概呈现出来是这个样子,2个Counter和2个Input共享一个外部状态,会跟随另一个的改变而改变。
而useSyncExternalStore可以简化useStore中我们的代码。
// 前面都一样,省略
import { useCallback, useSyncExternalStore } from 'react';
const useStore = (store, selector) => {
return useSyncExternalStore(
store.subscribe,
useCallback(() => selector(store.getState()), [store, selector])
)
}
效果完全一样,且我们不用操心内部的逻辑究竟是怎么样的。
在这个基础上,加上 shallow 和 middleware 的功能,基本就是 zustand 的源码了,用法也基本相似。
建议可以看看上面链接中的完整视频,就是zustand的作者发的,20分钟让我受益匪浅。
对比
春风得意马蹄疾,一日看尽长安花。
这周把主流数据流代表的源码翻了一遍。
本来都源码都看完的,结果recoil和jotai的代码实在太多了,直接放弃,但是我至少也翻了一遍,迅速把滚动条滑到底。
需求场景说明
对比场景就选用了大哥在大佬文章的评论里抛出的一个简化需求,如图。
简单来说,上面 +1 的按钮的操作,可以存档,并通过上面的undo和redo前进和回退,而下面的 +2 的按钮操作则无法存档。
各个库代码演示
第一个zustand的例子是为了更好看清楚UI层的组件里的一些操作,而且因为用了第三方的中间件,不放出来可能对其中的一些方法有些迷惑,后续的例子就不贴Component里的代码里,可以直接去sandBox里看,不然影响体验。
zustand
直接上大哥代码。
import create from "zustand";
import { undoMiddleware } from "zundo";
// 将原来的部分拆到单独的 slice 中,添加 middleware
const createUndoSlice = undoMiddleware((set, get) => ({
plus: () => {
set((s) => ({
...s,
data: s.data + 1
}));
},
data: 3
}));
export const useStore = create((set, get) => ({
tabs: "1",
switchTabs: (key) => {
set({ tabs: key });
},
plusWithoutHistory: () => {
set((s) => ({ ...s, data: s.data + 2 }));
},
...createUndoSlice(set, get)
}));
import "./styles.css";
import { Button, Space, Divider, Tabs } from "antd";
import "antd/dist/antd.css";
import { useStore } from "./store";
// application component
export default function App() {
const store = useStore();
const {
data,
plus,
redo,
undo,
tabs,
switchTabs,
plusWithoutHistory
} = store;
return (
<div style={{ padding: 24 }}>
历史记录:
<Space>
<Button onClick={undo}>undo</Button>
<Button onClick={redo}>redo</Button>
</Space>
<Divider />
<div>下面是历史记录里的部分</div>
<Space>
<Button onClick={plus}>+1</Button>
</Space>
<div>data: {data}</div>
<Divider />
<div>不在历史记录里的部分</div>
<div>
<Tabs activeKey={tabs} onChange={switchTabs}>
<Tabs.TabPane key={"1"} tab="数据"></Tabs.TabPane>
<Tabs.TabPane key={"2"} tab="配置"></Tabs.TabPane>
</Tabs>
<div>下面的 +2 可使得 在历史记录外添加让 data +2 </div>
<Button onClick={plusWithoutHistory}>+2</Button>
</div>
</div>
);
}
我觉得这个看起来虽然看起来简单,但是是把复杂度转移到了第三方开发的middleware中,具有一定的特定场景迷惑性。
react hooks + useSyncExternalStore
直接用上文中react写的store进行开发的话,把前进和后退的实现写了一下,测了一下也基本满足要求,如果用zustand实现的话,可能会在api上的调用会更简洁一点,但是应该大差不差,但是确实比上面中间件的方式多了许多代码。
import { createStore } from "./createStore";
let undo = [];
let redo = [];
export const store = createStore({
tabs: "1",
data: 3,
plusWithoutHistory: () => {
store.setState((state) => {
return {
...state,
data: state.data + 2
};
});
},
plus: () => {
store.setState((state) => {
const { data } = state;
undo = [...undo, data];
redo = [];
return {
...state,
data: state.data + 1
};
});
},
handleUndo: () => {
if (!undo.length) {
return;
}
store.setState((state) => {
const { data } = state;
const d = undo.pop();
redo = [...redo, data];
return {
...state,
data: d
};
});
},
handleRedo: () => {
if (!redo.length) {
return;
}
store.setState((state) => {
const { data } = state;
const d = redo.pop();
undo = [...undo, data];
return {
...state,
data: d
};
});
}
});
valtio
proxy写起来有种写vue的感觉,挺舒服的,而且实现起这个场景起来感觉特别简洁。
import { proxy } from "valtio";
const state = proxy({
data: 3,
tabs: "1"
});
let undo = [];
let redo = [];
export const store = {
state,
plus: () => {
undo = [...undo, state.data++];
redo = [];
},
plusWithoutHistory: () => {
state.data += 2;
},
handleUndo: () => {
if (!undo.length) {
return;
}
const d = undo.pop();
redo = [...redo, state.data];
state.data = d;
},
handleRedo: () => {
if (!redo.length) {
return;
}
const d = redo.pop();
undo = [...undo, state.data];
state.data = d;
}
};
recoil
看了半天文档,实在不知道咋写,直接放弃。
jotai
import { atom } from "jotai";
let undo = [];
let redo = [];
export const dataAtom = atom(3);
export const tabsAtom = atom("1");
export const plusDataAtom = atom(null, (get, set) => {
const data = get(dataAtom);
undo = [...undo, data];
redo = [];
set(dataAtom, data + 1);
});
export const plusWithoutHisDataAtom = atom(null, (get, set) =>
set(dataAtom, get(dataAtom) + 2)
);
export const handleRedoAtom = atom(null, (get, set) => {
if (!redo.length) {
return;
}
const data = get(dataAtom);
const d = redo.pop();
undo = [...undo, data];
set(dataAtom, d);
});
export const handleUndoAtom = atom(null, (get, set) => {
if (!undo.length) {
return;
}
const data = get(dataAtom);
const d = undo.pop();
redo = [...redo, data];
set(dataAtom, d);
});
hox
总算是看着文档写下来了,总体感觉竟然还不错。
写完有两个2个思考的点
- 先得写个自定义hook,虽然自由度比较高,但是感觉又把皮球踢到了使用方这里,hook里代码的复杂程度,好坏程度太难控制了。
- 要自己去组件外面写个provider有点麻烦
import { useState, useRef } from "react";
import { createStore } from "hox";
export const [useHisStore, HisStoreProvider] = createStore(() => {
const undo = useRef([]);
const redo = useRef([]);
const [data, setData] = useState(3);
const [tabs, setTabs] = useState("1");
const plus = () => {
redo.current = [];
undo.current = [...undo.current, data];
setData(data + 1);
};
const plusWithoutHistory = () => {
setData(data + 2);
};
const handleRedo = () => {
if (!redo.current.length) {
return;
}
const d = redo.current.pop();
undo.current = [...undo.current, data];
setData(d);
};
const handleUndo = () => {
if (!undo.current.length) {
return;
}
const d = undo.current.pop();
redo.current = [...redo.current, data];
setData(d);
};
return {
data,
tabs,
plus,
plusWithoutHistory,
handleRedo,
handleUndo
};
});
总结
- 基本新的库都用上了useSyncExternalStore,跟原来老的几个库相比,代码量大大下降,以后的库应该都会用上这个api,毕竟官方说了,不用的话临界情况会有bug🐶,就差刀架脖子上了。
- 每个库试用下来的感觉就是,valtio写起来最舒服,个人推荐valtio。
- hox的写法,虽然看起来有疑惑,但是写起来还行
- 如果zustand的话,我觉得不如自己原地写一个,反正核心代码就这样了,既然大家都爱造轮子,不如都造一个看看谁的滚的更远,万一自己的写好了说不定直接起飞,退一百步,就算实在是写烂了,再迁移到zustand,成本也是低的一批。
- recoil的话看文档是真的没有想好怎么写,放弃了。。
- jotai我感觉比较适合细粒度的组件使用,因为每一个action(setter)都需要在组件里useAtom一次,如果一个组件里包含了多个action,写起来就会很繁琐,相反,如果按照action把组件拆分得足够细,感觉使用起来会比较适合。
由于挑选场景的限制,和对一些库不是那么了解,很多总结可能会有点片面。有可能是文档没读仔细,没找到更好的方法,或者理解有偏差等等,希望大哥们可以指正。
todo
基本整个篇幅都在代码使用的层面,可能对日后新项目的选型有所帮助,但是现在需要解决的还是老项目中状态管理问题的解决。
最粗暴的就是,用微应用拆分,新的模块用新的状态管理,老的用dva,就像出了hook,老的class组件维护还是保持原来的写法,我管这个叫做代码断层。到后来,维护一个项目,我可能需要熟悉并掌握react上下10年的生态(夸张一下)。
由于dva在项目中对代码的入侵性真的很强(有可能是我们项目代码写太差了),还是需要想一个风险成本较小的替换方案。
参考(可能点不进去)
转载自:https://juejin.cn/post/7138789672095842335