背景: RecoilFacebook 推出的一款专门针对React应用的状态管理库,在一定程度上代表了目前的一种发展趋势,在使用时觉得一些理念很先进,能极大地满足作为一个前端开发者的数据需求,本文对 Recoil 的这些特性做一个梳理。

根据官网的介绍,Recoil 的数据定义了一个有向图 (directed graph),状态的变更是通过改变图的根节点 (atom),再通过纯函数 (selector) 流向 React 组件。

同时 Recoil 的状态定义是增量和分布式的,增量意味着我们可以在用的时候再定义新的状态,而不必将所有状态提前定义好再消费。分布式意味着状态的定义可以放在任何位置,不必统一注册到一个文件中。这样的好处是一方面可以简化状态的定义过程,另一方面也可以很好地应用在 code-splitting 场景。

在一个应用中开启 Recoil 非常简单,只需要包裹一个 RecoilRoot 即可。

import { RecoilRoot } from 'recoil';ReactDOM.render(<RecoilRoot> <App /></RecoilRoot>,root);

状态定义,原子和选择器

Recoil 允许使用 atomselector 两个函数定义基础和推导状态。

atom 基本用法,这里定义了相关的原子属性,需要使用唯一 key 来描述这个 atomRecoil 中不允许重复的 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>);
}

很多时候我们只想获取数据而不想修改,或者反之,此时可以用语法糖 useRecoilValueuseSetRecoilState

function UserProfile() {const firstName = useRecoilValue(firstNameAtom);return (<div> { firstName } </div>);
}

Recoil 会根据哪里用到了这些状态自动建立一种依赖关系,当发生变更时 Recoil 只会通知对应的组件进行更新。

selector 的用法和 atom 很像,构造一个 selector 至少需要一个唯一的 keyget 函数。

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

如果使用了一个异步的 atomselector ,则外层需要一个 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>);
}

通过把通用的 LoadingError 逻辑剥离出去,使得一般组件内的条件分支减少 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-routerhistory 同步到 atom 中,原生 js 动画库状态和 Recoil 同步,将 atom 和远程 mongodb 同步。通过直接读写 atom 就能直接读写外部系统,开发效率可以大大提高。

这种场景下可以借助 recoil-sync 这个包,下面列举两个案例。

使用 sharedb + recoil-sync 可以让 atommongodb/postgres 等数据库进行状态同步,从而让远程数据库修改如同本地修改一样方便。

// 对其的修改会实时同步到远程mongodb中
const [name, setName] = useRecoilState(nameAtom);

使用 recoil-syncatompixi.js 动画元素进行状态同步

codesandbox.io/s/nice-swir…

此时可以将画布上的一些精灵变成受控模式。

由于同步过程中会产生数据格式校验问题, recoil-sync 使用 @recoiljs/refine 用来提供数据校验和不同版本数据迁移功能。

Recoil 状态快照

由于状态粒度较细,对于需要批量设置 RecoilState 的场景, RecoilSnapshot 的概念,适合 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是一个纯函数。如果一个 selectorget 回调中存在网络请求,那就不再是一个纯函数,此时需要保证:网络请求是在所有异步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 状态管理方案的浅入浅出相关推荐

  1. websocket实践与浅入浅出

    websocket实践与浅入浅出 websocket与http的区别? websocket的应用场景? websocket通信方式 websocket协议结构 nginx配置 分布式下IM多端同步的实 ...

  2. [科普]浅入浅出Liunx Shellcode

    创建时间:2008-05-13 文章属性:原创 文章提交: pr0cess  (pr0cess_at_cnbct.org) 浅入浅出Liunx Shellcode /*---------------- ...

  3. 浅入深出之Java集合框架(上)

    Java中的集合框架(上) 由于Java中的集合框架的内容比较多,在这里分为三个部分介绍Java的集合框架,内容是从浅到深,如果已经有java基础的小伙伴可以直接跳到浅入深出之Java集合框架(下). ...

  4. Angular浅入深出系列 - 写在前面

    本系列目录: 写在前面 基础知识 控制器(Controller) 作用域(Scope) 集合(Collection) 模块(Module) 依赖注入(Dependency Injection) 服务( ...

  5. SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数式编程

    函数式编程(Functional Programming),一看这个词,简直就是学院派的典范. 以至于从 Lisp 的创世,到 Scheme.Haskell.Clean.Erlang.Miranda. ...

  6. 浅入浅出深度学习理论实践

    全文共9284个字,40张图,预计阅读时间30分钟. 前言 之前在知乎上看到这么一个问题:在实际业务里,在工作中有什么用得到深度学习的例子么?用到 GPU 了么?,回头看了一下自己写了这么多东西一直围 ...

  7. Spring浅入浅出——不吹牛逼不装逼

    Spring浅入浅出--不吹牛逼不装逼 前言: 今天决定要开始总结框架了,虽然以前总结过两篇,但是思维是变化的,而且也没有什么规定说总结过的东西就不能再总结了,是吧.这次总结我命名为浅入浅出,主要在于 ...

  8. 浅入浅出linux中断子系统

    浅入浅出linux中断子系统,如需深入,直接跳转重要参考章节. 什么是中断? 当CPU被某些信号触发,CPU暂停当前工作,转而处理信号的事件,简单的称它为中断,这个信号可以是系统外设的信号,也可能是芯 ...

  9. 看雪学院-浅入浅出Android安全 笔记

    Note 看雪学院-浅入浅出Android安全 原作地址:http://www.kanxue.com/?article-read-547.htm 翻译作者:飞龙 Android 由四个层组成:Linu ...

  10. SegmentFault 技术周刊 Vol.16 - 浅入浅出 JavaScript 函数式编程 1

    函数式编程(Functional Programming),一看这个词,简直就是学院派的典范. 以至于从 Lisp 的创世,到 Scheme.Haskell.Clean.Erlang.Miranda. ...

最新文章

  1. Tomcat unable to start within 45 seconds.
  2. python自学流程-python 学习之 基础篇三 流程控制
  3. es-04-mapping和setting的建立
  4. Windows Phone 实用开发技巧(9):自定义Windows Phone 页面切换动画
  5. 从程序员到项目经理(二十九):怎样写文档
  6. css 写打印样式问题
  7. c#后的完整cookie
  8. python基础篇 —— 类
  9. decimal.JS 快速入门
  10. excel表转成PDF文档
  11. [乡土民间故事_徐苟三传奇]第二回_巧答言长工骂财主
  12. matlab求解整数规划问题
  13. POJ 1273 EK算法
  14. matplotlib 柱状图画误差棒
  15. 网易云音乐歌单生成外链播放器
  16. Android版优酷网闪亮登场
  17. cardboard的使用
  18. 以人为本 体验至上(三)
  19. MATLAB 仿真 n年后的比例,基于Matlab的比例导引弹道仿真分析.PDF
  20. 圣诞树代码,c语言编程,基于graphics.h

热门文章

  1. 模拟版图layout能否转数字IC后端实现?
  2. 条形图的几种处理方式
  3. 家乡の战队实训项目博客二
  4. 主流安全大漏洞:隐私空间泄密
  5. 京东 Android 客户端样式的级联地址选择器
  6. python中seaborn报错These `style` levels are missing dashes解决办法
  7. EulerOS简介--openEuler
  8. 大数据时代,企业如何做好数据防泄漏?
  9. opencv-python 实现颜色检测
  10. DirextX7。0 SDK 在VC 6.0 环境中使用的注意事项