Recoil 状态管理方案的浅入浅出
背景: Recoil
是 Facebook
推出的一款专门针对React
应用的状态管理库,在一定程度上代表了目前的一种发展趋势,在使用时觉得一些理念很先进,能极大地满足作为一个前端开发者的数据需求,本文对 Recoil
的这些特性做一个梳理。
根据官网的介绍,Recoil
的数据定义了一个有向图 (directed graph),状态的变更是通过改变图的根节点 (atom),再通过纯函数 (selector) 流向 React
组件。
同时 Recoil
的状态定义是增量和分布式的,增量意味着我们可以在用的时候再定义新的状态,而不必将所有状态提前定义好再消费。分布式意味着状态的定义可以放在任何位置,不必统一注册到一个文件中。这样的好处是一方面可以简化状态的定义过程,另一方面也可以很好地应用在 code-splitting 场景。
在一个应用中开启 Recoil
非常简单,只需要包裹一个 RecoilRoot
即可。
import { RecoilRoot } from 'recoil';ReactDOM.render(<RecoilRoot> <App /></RecoilRoot>,root);
状态定义,原子和选择器
Recoil
允许使用 atom
和 selector
两个函数定义基础和推导状态。
atom
基本用法,这里定义了相关的原子属性,需要使用唯一 key
来描述这个 atom
。 Recoil
中不允许重复的 key 出现,包括后面提到的 selector
。
const firstNameAtom = atom({key: 'first name atom',default: ''
});const lastNameAtom = atom({key: 'last name atom',default: ''
});
使用时通过 useRecoilState
这个 hooks 获取状态,可以看到它和 useState
很像,所以可以很轻松地将传统的React状态迁移到 Recoil
中。
function UserProfile() {
-const [firstName, setFirstName] = useState('');
+const [firstName, setFirstName] = useRecoilState(firstNameAtom);return (<div> { firstName } </div>);
}
很多时候我们只想获取数据而不想修改,或者反之,此时可以用语法糖 useRecoilValue
和 useSetRecoilState
function UserProfile() {const firstName = useRecoilValue(firstNameAtom);return (<div> { firstName } </div>);
}
Recoil
会根据哪里用到了这些状态自动建立一种依赖关系,当发生变更时 Recoil
只会通知对应的组件进行更新。
selector
的用法和 atom
很像,构造一个 selector
至少需要一个唯一的 key
和 get
函数。
const nameSelector({key: 'my name selector',get: ({ get }) => {return get(firstNameAtom) + ' ' + get(lastNameAtom);}
});
在 selector
中可以读写任意 atom
/ selector
,没有任何限制。只有 get
方法的 selector
是只读的,如果需要可写,也支持传入 set
方法。
const nameSelector({key: 'my name selector',get: ({ get }) => {return get(firstNameAtom) + ' ' + get(lastNameAtom);},set: ({ get, set }, value) => { const names = value.split(' '); set(firstNameAtom, names?.[0]); set(lastNameAtom, names?.[1]);}
});
值得一提的是,selector支持从网络异步获取数据,这里才是有趣的开始,也是和其他状态管理的最大的不同,Recoil的状态不仅是纯状态,也可以是来自网络的状态。
const userSelector = selector({name: 'user selector',get: () => {return fetch('/api/user');}
});
使用 selector
时和 atom
一样可以通过 useRecoilState
, useRecoilValue
, useSetRecoilState
这几个 hook。
function App() {const user = useRecoilValue(userSelector);...
}
这样的特性使得我们的代码很容易重构,假如一开始一个属性是一个 atom
, 后面希望变成一个计算属性,此时可以很轻松地替换这部分逻辑,而无需修改业务层代码。
Recoil
还可以更强大,用下面一张图可以大致概括下,其完全可以当成一个统一的数据抽象层,将后端数据通过 http, ws, GraphQL 等技术映射到前端组件中。
atomFamily selectorFamily 批量创建状态的解决方案
在一些场景中会有需要批量创建状态的情况,我们会实例化多个相同的组件,每个组件都需要对应一个自己独立的状态元素,此时就可以使用 xxxFamily
api。
const nodeAtom = atomFamily({key: 'node atom',default: {}
});function Node({ nodeId }) {const [node, setNode] = useRecoilState(nodeAtom(nodeId));
}
可以看到,atomFamily
返回的是一个函数,而不是一个 RecoilState
对象。传入不同的 nodeId
会检查是否之前已存在,如果存在则复用之前的,不存在则创建并使用默认值初始化。
同理,对于 selectorFamily
。
const userSelector = selectorFamily({key: 'user selector family',get: (userId) => () => {return fetch(`/api/user/${userId}`);}
});function UserDetail({ userId }) {const user = useRecoilValue(userSelector(userId));
}
由于批量创建可能会导致内存泄漏,所以 Recoil
也提供了缓存策略管理,分别为 lru
, keep-all
, most-recent
,可以根据实际需要选取。
Suspense 与 Hooks
上文提到每个 atom
, selector
背后可以是本地数据,也可以是网络状态(对,没错, atom
也可以是个异步数据,常用的如 atom
初始化是个异步,后续变成同步数据),在组件消费时无需关心背后的实际来源,使用远程数据就像使用本地数据一样轻松。
来看一个普通的获取数据并展示组件的例子。
function getUser() {return fetch('/api/user');
}function LocalUserStatus() {const [loading, setLoading] = useState(false);const [user, setUser] = useState(null);useEffect(() => {setLoading(true);getUser().then((user) => {setUser(user);setLoading(false);})}, []);if (loading) {return null;}return (<div>{ user.name }</div>)
}
对于这种开发习惯 (往往被称为 Fetch-on-Render):我们需要一个 useEffect
来获取数据,再需要设置一些 loading
, error
状态处理边界状态,如果这个数据不是一个放在全局且处在顶层的数据,而是散落在子组件中消费,则每一个使用的地方都要执行类似的逻辑。
下面看下 Recoil
的写法
const localUserAtom = atom({key: 'local user status',default: selector({ // <-------- 默认值来自 selectorkey: 'user selector',get: () => {return fetch('/api/user');}})
});function LocalUserStatus() {const localUser = useRecoilValue(localUserAtom);return (<div>{ localUser.name }</div>)
}
这里在组件层是不关心数据从哪来的, Recoil
会自动按需请求数据。
相比之下,后者的代码就简洁许多(Render-as-You-Fetch),而且背后并没有发明新的概念,用到的都是 React
原生的特性,这个特性就是 Suspense
。
如果使用了一个异步的 atom
或 selector
,则外层需要一个 Suspense
处理网络未返回时的 loading
状态。也可以套一层 ReactErrorBoundary
处理网络异常的情况。
// UserProfile 中使用了一个需要从网络中加载的数据
function LocalUserStatus() {const user = useRecoilValue(localUserAtom);...
}function App() {return (<div><div>hello, 外部组件在这里</div><Suspense fallback={<Loading />}><LocalUserStatus /></Suspense><div>底部</div></div>);
}
通过把通用的 Loading
和 Error
逻辑剥离出去,使得一般组件内的条件分支减少 66%,首次渲染即是数据准备完成的状态,减少了额外的处理逻辑以及 hooks 过早初始化问题。
hooks 过早初始化问题可参考拙文: Recoil 这个状态管理库,用起来可能是最爽的
useRecoilValueLoadable(state) 读取数据,但返回的是个Loadable
和 useRecoilValue
不同,useRecoilValueLoadable
不需要外层 Suspense
,相当于将边界情况交给用户处理。
Loadable
的对象结构如下:
const userLoadable = useRecoilValueLoadable(userSelector);const isLoading = userLoadable.state=== 'loading';
const isError = userLoadable.state === 'hasError';
const value = userLoadable.getValue();
Recoil 用来映射外部系统
在一些场景下我们希望 Recoil
能够和外部系统进行同步,典型的例子例如 react-router
的 history
同步到 atom
中,原生 js 动画库状态和 Recoil
同步,将 atom
和远程 mongodb
同步。通过直接读写 atom
就能直接读写外部系统,开发效率可以大大提高。
这种场景下可以借助 recoil-sync
这个包,下面列举两个案例。
使用 sharedb
+ recoil-sync
可以让 atom
和 mongodb
/postgres
等数据库进行状态同步,从而让远程数据库修改如同本地修改一样方便。
// 对其的修改会实时同步到远程mongodb中
const [name, setName] = useRecoilState(nameAtom);
使用 recoil-sync
将 atom
和 pixi.js
动画元素进行状态同步
codesandbox.io/s/nice-swir…
此时可以将画布上的一些精灵变成受控模式。
由于同步过程中会产生数据格式校验问题, recoil-sync
使用 @recoiljs/refine
用来提供数据校验和不同版本数据迁移功能。
Recoil 状态快照
由于状态粒度较细,对于需要批量设置 RecoilState
的场景, Recoil
有 Snapshot
的概念,适合 ssr
时注入首屏数据,创建快照进行回滚,批量更新等场景。
填充 SSR 的数据
function initState(snapshot) {snapshot.set(atoms.userAtom, {name: 'foo',});snapshot.set(atoms.countAtom, 0);
}export default function App() {return (<RecoilRoot initializeState={initState}>...</RecoilRoot>);
}
应用数据回滚
function TimeMachine() {const snapshotRef = useRef(null);const [count, setCount] = useRecoilState(countAtom);const onSave = useRecoilCallback(({ snapshot }) => () => {snapshot.retain();snapshotRef.current = snapshot;},[]);const onRevoca = useRecoilCallback(({ gotoSnapshot }) => () => {if (snapshotRef.current) {gotoSnapshot(snapshotRef.current);}},[]);return (<div><button onClick={onSave}>save</button><button onClick={onRevoca}>recova </button><button onClick={() => setCount((v) => v + 1)}> add {count} </button></div>);
}
不使用 async-await也能实现异步转同步代码
在 React
的世界里一直存在着一种很奇怪的代码技巧,这种技巧能够不利用 generator
或者 async
就能达到异步转同步的功能,在了解 Recoil
的一些用法时我也留意到这种现象,很有意思,这里介绍下: 假如 userSelector
是一个需要从网络中获取的状态,对其的读取可视作一个异步操作,但是在写 selector
时我们可以以一种同步的方式来写。
const userNameSeletor = selector({key: 'user name selector',get: ({ get }) => {const user = get(userSelector);<--- 这里背后是个网络请求return user.name;}
});
这种写法之前出现过,在组件中使用 selector
时我们也没有考虑其异步性。
function UserProfile() {const user = useRecoilValue(userProfile); <---- 这里背后也是个网络请求const userId = user.id;return <div>uid: {userId}</div>;
}
在组件中使用时是利用了外层的 Suspense
执行,在上述的 get
回调中内部也隐式地使用了相似手段,当发生异步时 get
方法会将Promise
当成异常抛出,当异步结束时再重新执行这个函数,所以这个函数本身会执行两次,有点黑魔法的感觉,这也同样要求我们在此时应该保证get
是一个纯函数。如果一个 selector
的 get
回调中存在网络请求,那就不再是一个纯函数,此时需要保证:网络请求是在所有异步selector执行之后调用。
// 正确的用法
const nameSelector = selector({key: "name selector",get: async ({ get }) => {get(async1Selector);get(async2Selector);await new Promise((resolve) => {setTimeout(resolve, 0);});return 1;}
});// 错误的用法
const nameSelector = selector({key: "name selector",get: async ({ get }) => {get(async1Selector);await new Promise((resolve) => {setTimeout(resolve, 0);});get(async2Selector);return 1;}
});
最后,关于代码直觉,心智负担
最近很多人会讨论一个库是否适合引入时会说到这两个词,在对一个库不了解的情况下我们很容易就说出“这个库太复杂了”,“要记忆的api太多了” 这类的话。在 Recoil
的世界里如果我们接受了 atom
, selector
,那么 atomFamily
, selectorFamily
也很容易理解。由于已经习惯了 useState
那么 useRecoilValue
, useSetRecoilValue
也很容易接受, 都很符合 hooks 的直觉。
Recoil
的 api 和 react
自身的 useState
, useCallback
, Suspense
是概念一致的, 二者的使用反而会加深对 react
框架本身的理解,一脉相承,没有引入其他的编程概念,api虽多但心智负担并不大。举个反例,如果在 react
中使用 observable
类型的状态管理,我可能会思考 useEffect
在一些场景是否能够按预期工作,虽然某些特性使用起来很舒服,但却加深了心智负担。
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享
Recoil 状态管理方案的浅入浅出相关推荐
- websocket实践与浅入浅出
websocket实践与浅入浅出 websocket与http的区别? websocket的应用场景? websocket通信方式 websocket协议结构 nginx配置 分布式下IM多端同步的实 ...
- [科普]浅入浅出Liunx Shellcode
创建时间:2008-05-13 文章属性:原创 文章提交: pr0cess (pr0cess_at_cnbct.org) 浅入浅出Liunx Shellcode /*---------------- ...
- 浅入深出之Java集合框架(上)
Java中的集合框架(上) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到浅入深出之Java集合框架(下). ...
- Angular浅入深出系列 - 写在前面
本系列目录: 写在前面 基础知识 控制器(Controller) 作用域(Scope) 集合(Collection) 模块(Module) 依赖注入(Dependency Injection) 服务( ...
- SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数式编程
函数式编程(Functional Programming),一看这个词,简直就是学院派的典范. 以至于从 Lisp 的创世,到 Scheme.Haskell.Clean.Erlang.Miranda. ...
- 浅入浅出深度学习理论实践
全文共9284个字,40张图,预计阅读时间30分钟. 前言 之前在知乎上看到这么一个问题:在实际业务里,在工作中有什么用得到深度学习的例子么?用到 GPU 了么?,回头看了一下自己写了这么多东西一直围 ...
- Spring浅入浅出——不吹牛逼不装逼
Spring浅入浅出--不吹牛逼不装逼 前言: 今天决定要开始总结框架了,虽然以前总结过两篇,但是思维是变化的,而且也没有什么规定说总结过的东西就不能再总结了,是吧.这次总结我命名为浅入浅出,主要在于 ...
- 浅入浅出linux中断子系统
浅入浅出linux中断子系统,如需深入,直接跳转重要参考章节. 什么是中断? 当CPU被某些信号触发,CPU暂停当前工作,转而处理信号的事件,简单的称它为中断,这个信号可以是系统外设的信号,也可能是芯 ...
- 看雪学院-浅入浅出Android安全 笔记
Note 看雪学院-浅入浅出Android安全 原作地址:http://www.kanxue.com/?article-read-547.htm 翻译作者:飞龙 Android 由四个层组成:Linu ...
- SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数式编程 1
函数式编程(Functional Programming),一看这个词,简直就是学院派的典范. 以至于从 Lisp 的创世,到 Scheme.Haskell.Clean.Erlang.Miranda. ...
最新文章
- Tomcat unable to start within 45 seconds.
- python自学流程-python 学习之 基础篇三 流程控制
- es-04-mapping和setting的建立
- Windows Phone 实用开发技巧(9):自定义Windows Phone 页面切换动画
- 从程序员到项目经理(二十九):怎样写文档
- css 写打印样式问题
- c#后的完整cookie
- python基础篇 —— 类
- decimal.JS 快速入门
- excel表转成PDF文档
- [乡土民间故事_徐苟三传奇]第二回_巧答言长工骂财主
- matlab求解整数规划问题
- POJ 1273 EK算法
- matplotlib 柱状图画误差棒
- 网易云音乐歌单生成外链播放器
- Android版优酷网闪亮登场
- cardboard的使用
- 以人为本 体验至上(三)
- MATLAB 仿真 n年后的比例,基于Matlab的比例导引弹道仿真分析.PDF
- 圣诞树代码,c语言编程,基于graphics.h