前言
自我加入工作后,就一直在学习、使用 react。react 是流行的前端框架,极致简洁的 JSX 语法,深深的吸引着我,它像是一个好朋友,伴我同行,并肩作战。本文主要描述本人使用 react 进行开发,对状态管理以及数据流理解的心路历程。从组件状态、context、unstated-next 到 redux、mobx、rxjs、swr,作为自己这段时间的总结,希望大家能够有所收获。
历程
1. 初入 react,校场练兵,温柔乡
大学生时期的小陈,自学了前端技术,包括 html、css、js,还学习了 vue2.0 的基础知识,想着工作时候应该用得上。
刚加入工作的小陈,被告知公司前端主要使用的是 react,小陈心头一凉,“vue 不会白学了吧”。翻开 react 的官方文档,跟着教程做了做 demo,没想到2天的时间小陈就上手了 react(原来学习 vue 的经验并没有白费,很多概念、机制是相通的)。
2. 首秀之小试牛刀,心满意足
小陈自信心爆棚,开始从产品那里接一些小的需求,开发业务系统的前端,一开始只是显示一个列表,于是他放置了一个 antd 的 Table 组件,对着后端给出的 数据 API 文档,在页面组件 mount 生命周期那里,获取了的第一页数据并赋值给 list 状态,又加了个 page 状态,点击翻页按钮,改变 page 状态 并在 update 生命周期那里重新获取数据渲染。小陈自信地敲下 git commit、git push之后,就收工回家了。
const List = ()=>{
const [listState, setList] = useState([]);
const [page, setPage] = useState(0)
const columns = [
...
]
useEffect(()=>{
getList(page).then(res=>setList(res));
},[page]);
return <>
<Table dataSource={listState.list} columns={columns} />
<Pagination page={page} pages={listState.pages} onChange={setPage} />
</>
}
3. 警钟敲响,风雨将至
过了几天,产品又找到了之前做列表页面的小陈,说要加一个详情页,入口是在列表页面的每一项增加一个操作按钮,点击下钻到详情页。理解了需求,从后端那要到了数据接口,小陈马上动手,点击列表操作项,运用 react-router-dom,传入 id 参数进入详情页,加上新的路由,编写新的页面,获取明细数据,平铺展示,done。小陈没有意识到即将出现的问题,又自信地敲下 git commit、git push之后,开开心心收工回家了。
4. 敌人入侵,满城风雨
第二天,产品找上门来了,说道,”小陈啊,你这个列表页面有点问题,下钻详情页后,点击返回键回到列表页的时候,列表又回到了第一页,不方面逐个下钻查看,你修一下吧。“小陈心中咯噔一跳,出啥子问题了,连忙应了下来,马上修复。
小陈看了又看,百思不得其解,于是询问了旁边的前端大佬。大佬笑着说,”这不是很明显嘛,页面跳转的时候,组件卸载了,所以之前的状态没了,再一次返回列表页的时候,重置了状态,重新请求了数据,于是就形成了你看到的问题,你需要把状态变成全局的,这样它就不会被组件销毁啦,简单的问题,用 context 就可以解决了……不过……考虑到之后……“。
”好的,明白了,谢谢大佬。”小陈打断了大佬的话,没有继续听大佬说下去,回到了自己的工位上,赶紧翻开 react 文档,并快速找到了 context 的用法,并把它套在 App 上面。
<ListContext.Provider value={listState}>
<App/>
</ListContext.Provider>
并在列表组件那里拿出context的值并使用它。
const List = ()=>{
const {list, page, setList, setPage} = useContext(ListContext);
...
return ...
}
好的,问题解决了,赶紧上线。
5. 内忧外患,祸不单行
后面的几个星期里,小陈风雨无阻地开发着,需求一直在进行着,小陈发现,自己越来越多地使用了 context,有各种原因,为了共享状态、跨多层级的组件通讯、状态保持…… 这些 context,有的套在 App 上面,有的套在独立 Page 上面,都解决了问题,但是,由于逻辑上移,导致 App 和 Page 的代码量激增,一个文件放了几千行代码,各种状态,穿插着各种各样的 useEffect、useMemo,还不带重名的,每次 render 的时间隐隐感觉变长了,甚至已经出现了卡顿的感觉。小陈望着屏幕,滚动着深不见底的代码文件,谨慎的改着每一行代码,工作效率越来越低,身心无比疲惫。
// App.tsx
const stateA = ...;
const stateB = ...;
const stateC = ...;
const stateD = ...;
// ...
// ...
// 大量逻辑
<ContextA.Provider value={stateA}>
<ContextB.Provider value={stateB}>
<ContextC.Provider value={stateC}>
<ContextD.Provider value={stateD}>
...
<AppContent />
...
</ContextD.Provider>
</ContextC.Provider>
</ContextB.Provider>
</ContextA.Provider>
心中无比忐忑的小陈再一次走向了大佬的工位,让他看一看当前的形势。大佬皱了皱眉头,说:“一个 context 玩这么多花样,真亏你能写到现在。应用复杂起来了,状态逻辑已经超出 react 的极限了,你需要使用外部的状态管理库来处理这部分逻辑,接下来几天,你就不要接新的需求了,好好了解一下这方面的知识,重构一下之前的代码逻辑。”
6. 容器化 hooks,unstated-next
为了能够快速重构,并最大程度的复用到现有的代码,逻辑。小陈找到了 unstated-next 这个好帮手。
6.1 示例
unstated-next 使用核心方法的 createContainer ,传入一个自定义 hook,就能创建一个状态容器,使用 container.Provider 即可注入容器状态,底下的组件,即可使用 container.useContainer 获取容器内的共享状态。
import { createContainer } from "unstated-next";
import { useState } from "react";
const CounterContainer = createContainer((init: number = 0) => {
return useState(init);
});
function Demo() {
const [cnt, setCnt] = CounterContainer.useContainer();
return (
<div>
<button>{cnt}</button>
<button onClick={setCnt.bind(null, (a) => a + 1)}>inc</button>
</div>
);
}
function App(){
// 两个 Demo 共享同一状态
return <CounterContainer.Provider>
<Demo />
<Demo />
</CounterContainer.Provider>
}
为了避免上面的 context 一样的 JSX 嵌套,也可以为容器写一个组合方法
import { Container } from "unstated-next";
const CombineContainer: FC<{ containers: Container<any, any>[] }> = ({
containers,
children
}) => {
return (
<>
{containers
.reverse()
.reduce(
(acc, con) => createElement(con.Provider, { children: acc }),
children
)}
</>
);
};
const App = ()=>{
return <CombineContainer
containers={[
containerA,
containerB,
containerC,
containerD,
]}>
<AppContent />
</CombineContainer>
}
6.2 评价 unstated-next
优点
- 作为 context 状态管理的一种包装实现,没有额外的理解成本。
- 极其轻量,仅使用 hooks,上手成本低,简单易操作。
缺点
- 没有状态族(family)。
- 不同 container 相对独立,难以描述多个 container 间的状态依赖关系。
7. 军机大臣,redux
完成了一轮重构之后,小陈发现 react 社区的状态管理方案,还是以 redux 为主,为了让自己的项目代码更加贴合社区的主流,小陈还是决定学习、实践一波 redux。
打开 redux 的官网,紫色的底色中,小陈看到了专业的气质,其 github 50k 的 star 数给了小陈很强的安全感,说明社区十分认可 redux 的解决方案,安排了。
7.1 概念
在随后的学习之中,小陈接触到了 redux 当中的 state、reducer、action等概念。有以下核心概念:
- 单一数据源,状态只保存在一个单一的状态树中,不会有状态复制同步的问题。
- 状态是只读的,UI 组件只订阅状态,渲染视图,不会直接修改状态树。
- 函数式更新,组件通过触发事件来让 redux store 执行 reducer 中的状态更新逻辑,不修改原对象,而是返回新的对象。
总结来看就是:state = reducer(state, action)。讲的就是 state 由 reducer 生成,也由 reducer 更新,action 代表通用的事件。事件到达时 state 会被 reducer 更新,通过这种方式不断进行 state 的迭代并推送给订阅者(或组件),视图只能通过 dispatch(action) 的方式向 redux store 请求更新 state,因此 state 只能按照 reducer 知道的方式进行更新,而不给视图随意操作 state 的可能,这就是读写分离。
可算是摸到了军机大臣的套路,该让他干活了。
7.2 示例
那啥,给我拿个数据呗。redux 感到莫名其妙:“懂不懂规矩,兵马未动粮草先行,先把 reducer 给我。”
const listReducer = (state = {list:[], page:0, pages:0}, action)=>{
switch(action?.type){
case 'LIST_LOADED':
return {
...state,
list:action.list,
pages:action.pages
}
}
return state;
}
行了,拿数据吧。redux:“那啥,钱是有了,但是征兵(异步流程)的事儿,总不能我亲自去干吧,你得把人送到我门口,我来给清点一下。你如果不想做的话,可以再招个官员去做这件事,不过恰好,我认识几个亲戚,它们是专业取数的,它们是 redux-saga、redux-thunk、redux-observable,你挑一个招吧,但是只能招一个,因为它们关系不太好,合不来”。
好家伙,原来军机大臣是个贪官,平时抠门也就算了,还拖家带口的。小陈心中升起了烦躁,就 redux-saga 吧。很快 redux-saga 就开设了自己的府邸,接受 redux 的指示(action),开始了招兵(取数)计划,不得不说,这小伙子手段丰富,一手 yield + 各种手段(effect creators) 专治各种不服,最后把人全都带到(put) redux 门前。redux 看着门前井然有序的队伍,满意地登记入册。
function* fetchList(action) {
try {
const user = yield call(Api.fetchList, action.page);
yield put({type: "LIST_LOADED", user: user});
} catch (e) {
yield put({type: "LIST_FAILED", message: e.message});
}
}
function* mySaga() {
yield takeEvery("LIST_REQUESTED", fetchUser);
}
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
listReducer,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(mySaga)
小陈看数据都到位了,赶紧又加派了官员 react-redux 输送士兵(又一个亲戚)前往前线作战,将 context 的党羽替换下去。
const list = useSelect(s=>s);
const dispatch = useDispatch();
const setPage = (n) => dispatch({type: 'LIST_REQUESTED', page:list.page});
return <>
<Table dataSource={listState.list} columns={columns} />
<Pagination page={page} pages={listState.pages} onChange={setPage} />
</>
随后几个月,小陈都在与军机大臣 redux 打交道,发现他的风格就是这么谨小慎微,做事一丝不苟,从不露出任何破绽,管不过来了就找了儿子 combineReducers 分着管。小陈还优化了一下 redux 的办事效率,比如 RTK 用于简化 reducer 和 action creator。
7.3 评价 redux
a. 优点
- 代码规范,模板化的方式编写状态逻辑,逻辑清晰,不同人也可以写出一致的代码。
- 一致性、可预见性、易维护(得益于清晰的分层分离)。
- 工具支持好,有 redux-devtool 插件可以查看全局状态,并能回放应用,复现问题。
b. 缺点
- 开发效率低,样板需要慢慢写,最难的事情大概就是给 action 起名字。
- 性能优化问题需要工具和技巧。
- 文档太多太长,副作用领域的玩法太多了。
8. 骠骑大将军,mobx
在一次交际之中,小陈发现了勇士 mobx,它长得十分朴实、做事干净利落。小陈心想,这不就是我要找的良将吗。带上它正好可以压一压军机大臣 redux 的气焰,还不用浪费那么多军粮(模板代码)。
8.1 特点
mobx 与 redux 的风格完全不同,利用了对象属性访问拦截的特性,实现了响应式,通过执行代码访问属性,就可以自动注册副作用。
mobx 身披宝甲,坐镇军营(observer),新来的士兵(observable)一点都不敢偷懒,老老实实上阵地干活。
8.2 示例
// 定义可观察对象
// 全局的 observable
const global = observable({
cnt: 0,
inc() {
this.cnt++;
}
});
const Demo = observer(() => {
// 局部的 observable
const state = useLocalObservable(() => ({
age: 18,
name: "Mike"
}));
return (
<div>
<input
value={state.name}
// 直接对 observable 的属性赋值
onChange={(e) => (state.name = e.target.value)}
/>
<input
type="number"
value={state.age}
onChange={(e) => (state.age = Number(e.target.value))}
/>
<button onClick={global.inc.bind(global)}>{global.cnt}</button>
</div>
);
});
export default function App() {
return (
<div className="App">
<Demo />
<Demo />
</div>
);
}
得到了如下的结果,局部 state 是隔离的,global 是共享的。
8.3 评价
优点
相比 redux,mobx 操作简洁,可以直接修改对象并获得高效精准的组件更新,易于性能调优。
缺点
- 理念 与 react 是相悖的,因此需要时刻谨记这一点,并在两种范式之间切换。
- 自由也是有隐患的,mobx 由于是直接操作可观察对象,因此状态更新操作没有强行收敛,当代码不规范时,难以排查问题。
9. 明枪易躲暗箭难防
有一天,后端同学找到小陈,说:“小陈,之前有一个接口,使用用户的 id 去请求用户的头像,你看看这个列表,一页 20 项,你发了 20 次请求了,这样不行,对接方可能会加限制,我做了一个批量传 id 获取头像 url 的接口,你把接口换一下,过几天原来的接口我也要下线了。” 小陈当时没有细思里面的逻辑,于是应了下来,记了个 Todo。
过了几天,小陈终于排到了这个问题。于是他找到之前的头像组件:
const OurAvatar = ({id}) => {
const {value, loading, error} =
useAsync(()=> getAvatar(id), [id]);
return loading ? <PlaceHolderAvatar loading/> :
value ? <Avatar src={value.url} /> :
err ? <Error />
: <PlaceHolderAvatar />;
}
看着自己之前写的代码,小陈倍感欣慰,我写的代码,优秀~
于是就开始思考接口替换的事情:请求的逻辑是放在组件里面的,改成批量的…… 小陈细思极恐,难道说,不会吧,不会真的要把批量获取头像的逻辑弄到组件外面去,然后通过 props 下传吧。全局搜索过后,小陈发现这个组件已经被使用了几十次了,如果都要这么改,会在外面对应写上几十次批量查询逻辑…… 也没啥,就感觉头有点秃。
吃过之前像 context 那样上提状态的亏,小陈嗅到了一丝阴谋的气味,不敢再冒然动手了。找了 redux 和 mobx,得到的说法是这不属于全局状态,同时看起来也不像组件状态,也就是说,处理这些市井小混混,不在它们的业务范围内。小陈薅了薅稀疏的头发,觉得后端同学这波操作有点不厚道,一头倒在了工位上,真想撒手不管。目眩神迷之际,小陈看到了镖师 rxjs 的名片,原本灰暗的眼神中闪动着希望的光芒。
10. 金牌镖师,rxjs
在一次偶然的机会中,小陈碰到了 rxjs,其通红的身躯、高冷的气质在告诉自己,这个家伙不同寻常啊。
10.1 基本概念
rxjs 是基于发布订阅模式衍生出来的一套 ReactiveX API 的 js 实现版本。
其异步数据流十分灵活,大量内置的操作符能够大幅减少开发复杂数据流的思维负担,包括流生成、加工、分流、组合。
使用 rxjs 只需要关注 数据源、转换、订阅。
a. 数据源
- 自定义 Observable,提供了固定逻辑的事件源
- Subject,又称 EventEmitter,手动式的事件源
- BehaviorSubject,必有一个值的 subject
- ReplaySubject,带有一定 buffer 的 subject
- creator operators,可以将从其他结构(数组,定时器,promise,dom event)中生成 Observable
b. 转换
即各种 operator,从源 observable 中获取元素,经过转换后交给下一个操作符 或者 observer。开启转换的方式是调用 observable.pipe。
自定义操作符,使用柯里化的方式创建的操作符,具有可复用性。
// 1. 使用组合操作符的方式
// 从流中经过map取出一个值,并做相邻去重
export const select = <T = any, R = any>(mapper?: (a: T) => R, deep = true) => (source: Observable<T>): Observable<R> =>
source.pipe(map(mapper || _.identity), distinctUntilChanged(deep ? _.isEqual : _.eq));
// 2. 使用 new Observable 的方式
// 窗口缓存
export const batchTime = <T>(duration: number) => (observable: Observable<T>): Observable<T[]> =>
new Observable(observer => {
let buffer: any[] = [];
let timer: any = null;
const subscription = observable.subscribe({
next(value) {
buffer.push(value);
if (!timer) {
timer = setTimeout(() => {
timer = null;
observer.next(buffer);
buffer = [];
}, duration);
}
},
error(err) {
observer.error(err);
},
complete() {
observer.complete();
},
});
return () => {
subscription.unsubscribe();
clearTimeout(timer);
};
});
c. 订阅
有多种方式可以用于订阅
- observable.subscribe(observer)
- observable.toPromise()
- observable.forEach(callback)
10.2 问题分析&解决
想到这里,小陈立即找到 rxjs,让它分析一下这个问题。“货物分散走位可能会有回不来的情况,集合出行又大动干戈。这样吧,把货分散地送到我这里来,我每隔一段时间为你批量打包集装箱发送一次,货物回来时,我再给你做分拣,不过你自己要让供应商把货物标记好”。
用 rxjs 包装了一个简单的闭包。
(batchTime 是自己实现的操作符,从源接收到数据后,开启一个新的缓存数组,缓存一段时间的元素,然后将数组交给下一个操作处理)
interface createBatchReqParam<P, P2, R, R2> {
duration?: number;
mergeParams(params: P[]): P2;
fetcher(params: P2): Promise<R>;
resolver(res: R, param: P, resolve: (res: R2) => void, reject: (err: any) => void): boolean | void;
}
// 创建批量请求,对外单独调用,按一定时间将参数合并请求,将返回结果分流
export const createBatchReq = <P, P2, R, R2>({
duration = 100,
mergeParams,
fetcher,
resolver,
}: createBatchReqParam<P, P2, R, R2>) => {
const in$ = new Subject<{ param: P; resolve(res: any): void; reject(err: any): void }>();
in$.pipe(batchTime(duration)).subscribe(async arr => {
try {
const res = await fetcher(mergeParams(_.map(arr, e => e.param)));
arr.forEach(e => {
resolver(res, e.param, e.resolve, e.reject);
});
} catch (err) {
_.map(arr, item => item.reject(err));
}
});
return (param: P) =>
new Promise<R2>((res, rej) => {
in$.next({ param, resolve: res, reject: rej });
});
};
小陈提供了打包和分拣的逻辑。
const getAvatar = createBatchReq<string, { ids: string[] }, Avatar[], Avatar>({
duration: 24,
mergeParams(ids) {
// 将缓存的数组打包成一个参数
return { ids };
},
fetcher(params) {
// 用 mergeParams 的结果发起批量请求
return getBatchAvatars(params).then(res => res.data);
},
resolver(res, param, resolve, reject) {
// 拆开响应,分拣:匹配 id,resolve 结果
const item = res.find(e => e.id === param);
if (!item) {
reject(new Error('not found'));
return;
}
resolve(item);
},
});
像这样重新实现了 getAvatar,当 OurAvatar 批量挂载的时候,每 24ms 就会有一次批量请求,而 OurAvatar 组件完全没有改动到,nice。
10.3 杂谈
学习的过程中,得知 rxjs 竟能轻易实现 redux 的逻辑。rxjs 实现 redux (Youtube)
10.4 评价
优点
功能强大、灵活,对流的操作是其最大的亮点。
缺点
- 学习成本很高,操作符需要多训练方能熟练掌握。
- 目前并没有很好的与 react 的整合方案,因此只在一些小方法上利用其灵活的数据流操作简化代码。
11. 仓库管理员兼职采购员 swr
11.1 新的挑战
小陈在进一步的实践中,发现了 OurAvatar 每次挂载都会发请求,哪怕之前已经有一个相同 id 的头像在别的地方挂载过。其实对于 id 查头像这个接口来说,是十分稳定的结果,多次挂载是可以缓存复用之前的数据的。
11.2 方案
就在一筹莫展的时候,useSWR 引起了小陈的注意,它拥有一个很神奇的特质,只要给 useSWR 下好订单(key、fetcher),给个合适的过期时间(dedupingInterval),它那里有货就会直接给出,没货就帮你去拿货(fetch)并保管一段时间。太棒了,小陈心想这就是我要的专业人士,于是照着 useSWR 的规矩,尝试向它下了订单。
const OurAvatar = ({id}) => {
const {data, isValidating, error} =
useSWR(
()=>['avatar', id], // key
(_k,id)=> getAvatar(id), // fetcher
{
dedupingInterval: 3600 * 1000 // 缓存时间
});
return data ? <Avatar src={data.url} /> :
err ? <Error /> :
<PlaceHolderAvatar />;
}
11.3 深入了解
小陈对 useSWR 的这种做事模式十分感兴趣,于是继续向它请教,寻求更加深入的合作可能性。
const { data, error, isValidating, mutate } =
useSWR(key, fetcher, options)
其中参数有
- key,用于缓存键
- fetcher,获取数据的逻辑
- options 用于控制状态缓存的行为,其中几个常用的参数
- initialData,初始状态
- dedupingInterval,缓存时间
- isPaused,控制重新获取数据的开关
- refreshInterval,定时触发 revalidate,重新 fetch 返回的接口有
- data,当前的数据
- error,错误信息
- isValidating,是否在重新运行 fetcher 的过程中
- mutate,命令式的修改 data
11.4 已知用法
a. 强制更新
调用 mutate 即可
const {data, mutate} = useSWR(key,fetcher,options);
mutate(); // 重新调用 fetcher,不应与 isPaused true 同时使用
mutate(newData, false) // 手动给出新状态
mutate(async(data)=>{ // 计算获得并更新
...
return newData;
},false);
b. 全局状态
const cacheTime = 100000000_000; // 一个超长的缓存时间
function useGlobal() {
const global =
useSWR(
"global", // key,给状态起个名字
() => 0, // 直接返回初始值
{ dedupingInterval: cacheTime });
return {
data: global.data,
// 通过封装 mutate 对外暴露 action creator
inc: () => global.mutate((d) => d! + 1, false),
dec: () => global.mutate((d) => d! - 1, false)
};
}
c. 条件请求 & 状态依赖
这里利用了 key 的几种用法
- key 可以是 string、数组、[返回string或数组的函数]
- 函数返回 string 或者数组时正常请求
- 函数返回 null 或者抛出错误时,不请求
useSWR(()=> null, fetcher, options); // 不请求
useSWR(()=> url , fetcher, options); // fetcher(url)
useSWR(()=> [a,b,c], fetcher, options); // fetcher(a, b, c), 数组会铺进 fetcher 调用中
d. memo
useSWR(()=>[...依赖的状态], ()=>{...同/异步计算逻辑}, options)
e. 附属状态(nice)
- object 可以作为数组 key 的一部分,其内部会使用 WeakMap 生成 id
- 实践中发现比较有用的玩法,附属状态 与 obj 相关,但是不能放在 obj 上面
- 比如服务器返回了一个列表数据,每一项 obj 需要关联一个 UI 状态,如折叠面板组件的打开关闭状态
- 可以就近管理 UI 状态,并全局缓存,当页面回退时,状态依旧有效
const other = useSWR(() => [obj], {
initialData: false,
isPaused(){return true},
}
return {
state: other.data?.expand,
toggle: ()=> other.mutate(d=>!d, false),
}
f. 无限滚动加载
useSWR 还有个兄弟叫 useSWRInfinite。可以在一次调用里面去做批量的请求缓存。
g. 名称空间很重要
使用 useSWR 时,最好直接使用函数 key 并返回加上前缀的数组,防止键冲突的发生。实际上,我使用了文件名 + 状态名的方式来管理(傻瓜操作)。
function useUserInfo(id){
const userInfo =
useSWR(() => ['user/info', id],
(_k,id) => fetchUser(id),
options);
return userInfo.data;
}
h. 封装&复用
前面的 useGlobal、useUserInfo 就是一个使用 useSWR 封装 hooks 的例子
- useGlobal 中 useSWR 的 key 是确定的,所以是一个全局唯一的状态,使用到的地方都共享了这个状态,调用 mutate 更新时,也都能够通知到所有用到它的组件去 rerender。
- useUserInfo 需要传递 id 参数,当传入 id 相同时,组件访问的就是同一个状态,当状态更新时,共享同一状态的组件都会 rerender。
// example
function useAvatar(id){
return useUserInfo(id)?.avatarUrl;
}
const Avatar :FC<{id}> = ({id}) => {
const url = useAvatar(id);
return url ? <RoundImg src={url} /> : <PlaceholderAvatar />
}
11.5 评价 swr
优点
- 声明式的缓存机制。
- 对比其他状态管理库,swr 很简洁,学习上手成本很低。
- 直接提供了 hooks 接口,贴合 react 的用法,这是面向未来的。
- 封装出自定义的 hooks,极易复用、再封装。
- 面向资源,无 context 注入,敏捷开发。
缺点
api 过于简洁,缺失了一些功能(生命周期等),以后应该会完善。如果发现有功能满足不了,也可以尝试 react-query。
总结
没想到在过去一年的时间里,学了那么多东西,随着自己对前端的状态交互模式理解的不断加深,技术也在一步步演化迭代,趋向成熟。这些技术都有存在的合理性,在不同的业务场景下,可能会有不同的解法,有些技术,已经不再被纳入我的选择当中,还是需要结合业务选择最适合自己团队的工具、技术栈。面对不断发展的技术,需要多积累、多跟进、多思考,面对未来的技术挑战才能更加从容自信。