【译文】探索Recoil中的异步请求
这篇文章中,我将仔细研究Recoil中的异步查询。我将向你展示该库的一些功能以及它如何与React无缝结合。
我们开始吧。
Recoil是什么
Recoil是一个状态管理库,将状态映射到React组件。当状态是异步的时候,selectors将会在数据流中表现的像一个纯函数。编程接口依然是熟悉的样子,但是它将会返回一个Promise而不是一个值。
消费组件从感觉像是同步查询的seloctor函数中获取它们所需要的值。这允许我们使用一些强大的技术,比如React Suspense加载器,缓存,和预取请求。
设置
你可以在Github上找到实例代码。我建议你clone并运行下,以便更好的了解扎个库。我使用 json-server
来托管鲸鱼物种的数据。这个应用加载一些可选的鲸鱼物种然后你可以选择其中一个物种来查看其更多详细信息。AJAX请求提供app中的状态,Recoil映射异步的状态到React组件中。
我将引用大量的实例代码。要启动这个项目,在命令行中依次运行: npm run json-server
npm start
. API响应中有个3秒的延迟来说明Recoil中的功能。你可以在package.json中查看延迟,使用 30001 端口
托管数据。并设置一个代理 http://localhost:3001
。这使得CRA应用知道去哪里获取异步的数据。代码将引用 /whales/blue_whale
作为获取数据的路由,没有主机和端口。
React Suspense
<Suspense />
组件声明性的等待数据加载并定义了一个loading状态。当状态为异步时,Recoil钩入React组件。如果一个异步请求没有被包到Suspense中将无法编译。不过有一个解决办法就是 使用useRecoilValueLoadable
,但是这种情况下需要更多代码来追踪状态:(博主之前就是使用这种方法,要写很多代码,实例如下)
function UserInfo({userID}) {
const userNameLoadable = useRecoilValueLoadable(userNameQuery(userID));
switch (userNameLoadable.state) {
case 'hasValue':
return <div>{userNameLoadable.contents}</div>;
case 'loading':
return <div>Loading...</div>;
case 'hasError':
throw userNameLoadable.contents;
}
}
Recoil可以依赖Suspense组件,写法如下:
<RecoilRoot>
<Suspense fallback={<div>Loading whale types...</div>}>
<CurrentWhaleTypes />
<Suspense fallback={
<div>Loading <CurrentWhaleIdValue /> info...</div>
}> {/* nested */}
<CurrentWhalePick />
</Suspense>
</Suspense>
</RecoilRoot>
fallback
声明了加载组件,也可以是其他使用Recoil状态的组件。加载器可以被嵌套,所以一次只显示一个加载器,这个声明式定义了如何以及何时在app中加载数据。
当你选择一个鲸鱼,<CurrentWhalePick />
开始加载,CurrentWhaleIdValue
组件在 fallback
加载器中。
function CurrentWhaleIdValue() {
const whaleId = useRecoilValue(currentWhaleIdState)
return (
<span>{whaleId.replace('_', ' ')}</span>
)
}
currentWhaleIdState
是一个Recoil atom,它是查询参数的真是来源。选择一个鲸鱼设置鲸鱼id,这个值将会显示在加载器中。我鼓励你查看currentWhaleIdState
是如何定义的,因为这是Recoil去缓存请求的依赖。
CurrentWhalePick
组件通过一个查询selector获取异步状态。Recoil有useRecoilValue
钩子:触发最初的请求,当组件没有包裹在<Suspense />
中时会抛出一个错误。
很棒的一点是,useRecoilValue
钩子可以被用来调用同步数据的selectors。一个关键的不同就是同步调用不用被包裹在Suspense组件中。
function CurrentWhalePick() {
const whale = useRecoilValue(currentWhaleQuery) // fire AJAX request
return (
<>
{whale === undefined
? <p>Please choose a whale.</p> // zero-config
: <>
<h3>{whale.name}</h3>
<p>Life span: {whale.maxLifeSpan} yrs</p>
<p>Diet: {whale.diet} ({whale.favoriteFood})</p>
<p>Length: {whale.maxLengthInFt} ft</p>
<p>{whale.description}</p>
<img alt={whale.id} src={whale.imgSrc} />
</>
}
</>
)
}
当鲸鱼是undefined
时,app是零配置状态。当一切加载完毕时,最好向用户指示下一步要做什么(提示用户选择一个鲸鱼物种),而Recoil状态使这一切变得很easy。
缓存
Recoil selector函数是幂等的,也就意味着一个给定的输入肯定会返回相同的值。在异步请求中,这个变得相当重要因为响应可以被缓存。
当selector函数获取到参数依赖,Recoil自动缓存响应。下次组件用相同的输入请求时,selector返回一个fullfilled状态的带有缓存数据的Promise。
这就是Recoil如何触发一个请求并自动缓存响应。
const currentWhaleQuery = selector({
key: 'CurrentWhaleQuery',
get: ({get}) =>
get(whaleInfoQuery(get(currentWhaleIdState)))
})
查询参数currentWhaleIdState
是一个atom:返回一个基础字符串类型。Recoil做一个简单的相等检查当它被设置到缓存key查找表中时。如何参数依赖是被一个复杂类型包裹,然后相等操作符无法识别缓存key,这会破坏缓存。这是因为js中的相等检查时,对象每次的指针都是不同的。Recoil中的状态突变都是不可变的,对复杂类型的更改会设置一个新的实例。
一个建议是设置参数为简单类型来启用缓存--避免复杂类型除非你计划每次都破坏缓存。
如果你正在跟随正在运行的app,例第一点击Blue Whale需要3秒才能加载。点击其他鲸鱼种类也是需要等3秒,但是如果再次点击之前点过的,它会直接加载。这就是说Recoil缓存机制在起作用。
预取请求
Recoil使得当点击时间触发时立即触发请求成为可能。在React组件生命周期中不会执行请求直到传入了一个新的鲸鱼id参数触发组件渲染。这有一个小小的延迟在点击事件和ajax请求之间。鉴于网络上的异步请求速度较慢,尽快开始执行请求是有好处的。
为了使得查询能够预取,使用 selectorFamily
而不是 selector
:
const whaleInfoQuery = selectorFamily({
key: 'WhaleInfoQuery', // diff `key` per selector
get: whaleId => async () => {
if (whaleId === '') return undefined
const response = await fetch('/whales/' + whaleId)
return await response.json()
}
})
selector函数使有一个来自于前一个selector中get
的whaleid
参数。这个参数和 currentWhaleIdState
atom有相同的值。key
在不同的selector中时不同的,但是whalse id参数是相同的。
whaleInfoQuery
selector可以在你选中一个鲸鱼后立马触发请求因为它的函数参数中有whaleId
参数。
接下来:事件处理程序从e事件参数中获取鲸鱼id:
function CurrentWhaleTypes() {
const whaleTypes = useRecoilValue(currentWhaleTypesQuery)
return (
<ul>
{whaleTypes.map(whale =>
<li key={whale.id}>
<a
href={"#" + whale.id}
onClick={(e) => {
e.preventDefault()
changeWhale(whale.id)
}}
>
{whale.name}
</a>
</li>
)}
</ul>
)
}
这个组件渲染鲸鱼列表。它循环响应并且设置一个事件处理函数在html元素上。preventDefault
编程式地阻止浏览器把它当成一个实际的a标签跳转连接。
changeWhale
函数会立即从选中的元素捕获鲸鱼id并且变为最新的参数。一个useSetRecoilState
钩子也会改变鲸鱼id,但会将请求延迟到下次渲染。
因为为更喜欢用预取请求,changeWhale
函数如下:
const changeWhale = useRecoilCallback(
({snapshot, set}) => whaleId => {
snapshot.getLoadable(whaleInfoQuery(whaleId)) // pre-fetch
set(currentWhaleIdState, whaleId)
}
)
获取snapshot 并使用 set 立即改变状态,并使用查询和参数调用 getLoadable以触发请求。set和getLoadable之间的顺序无关紧要,因为whaleInfoQuery已经使用了必要的参数调用了查询。当组件重新渲染时,set保证了鲸鱼id的变化。
为了证明预取是有效果的,在调用fetch时在whaleInfoQuery中设置一个断点。检查调用堆栈并在堆栈的地步查找CurentWhaleTypes———这将执行onClick事件。如果恰好是CurrentWhalePick,则请求会在重新渲染时触发,而不是在点击事件中触发。
可以通过 useSetRecoilState
和 changeWhale
在预取和重新渲染之间交换查询。github上的已经注释掉可交换代码。我建议你把玩以下:交换重新渲染并查看调用堆栈。改回预取调用调用来自点击事件的查询。
总结
这就是demo的最终效果
总之,Recoil有一些优秀的异步状态特性:
- 通过
<Suspense />
包装器和后备组件与 React Suspense 无缝集成以在加载期间显示 - 自动缓存数据,假设selector函数使幂等的
- 发生点击事件时立即触发异步查询,以提升性能