本文旨在介绍
乐观更新,react query 和 zustand/jotai
reactquery 可以很好的做到一次性乐观更新,整个流程就是 mutate->success/error->settled
mutate,开始更新乐观更新,success 之后什么也不做,error 就将乐观更新的内容进行回退,最后 settled 重新获取数据
这里有几个问题
对于乐观更新的这个数据,怎么展示。
- 像其他正常数据一样展示。
- 展示一个请求的过程 (半乐观)
对于原有的分页数据,怎么跟乐观更新搭配
对于一次乐观更新,有几种状态。
乐观更新 -> 成功
乐观更新 -> 失败
失败的乐观更新 -> 重试乐观更新
交互上有几种策略
- 直接乐观更新,作为正常数据对待。失败就回退,不提示用户
- 乐观更新,展示正常数据,失败回退,提示用户
- 乐观更新,展示为发送中的数据,失败回退,提示用户
- 展示为发送中的数据,失败不回退,而是提示用户重新发送。
- 初次乐观更新视为正常数据。发送失败不回退,进行提示,用户可以交互重试,重试的时候展示发送状态。
失败回到上一流程,成功则更新
最复杂的,用户体验也最好的可能就是第五种,这也是微信 / QQ 这类 IM 使用的策略
以上是对一个 list 中新增项,除此之外,还有对某项的更新对 list 中某一项进行更改,还有对 list 中内容的删除
这时候,在客户端状态中,有几类数据
- 来自于服务器的有效数据
- 从客户端发送的单个数据
2.1 发送中
2.2 发送成功
2.3 发送失败 - 从客户端并发发送的多个数据。
客户端数据相比于服务端数据有些不同,比如缺少 id,多了一个状态标识。
但是缺少的 id 又不能乱给,因为更新成功后,服务端数据如果要重新获取,我们希望,服务端新增的数据能够准确替换乐观更新填充的数据
在客户端可能还需要对拿到的 list 内容进行一遍处理,与客户端的内容进行区分。
如果显示要进行标识。
回到上面的问题,以 react-query 介绍的方案为例
- 通过 ui,通过 mutate 的 pending 状态,结合传入 mutate 的参数,进行更新,然后重新获取数据,这是上面第一种策略
- 通过 ui,通过 mutate 的 variable,iserror,加上错误提示,允许重试
以上这两种都出现在简单局部情况下,对于数据消费组件,除了要感知 query,还必须要感知到 mutation,即使 mutation 跟组件不在一起,还是需要通过 mutationstate 获取 mutation 的状态
- 更新 cache,在 mutate 内部,onmutate 的时候先乐观更新数据,setquerydata
然后结束按需对乐观更新到数据操作,最后重新验证数据。
这个流程对于单个数据很不错
但是如果要展示发送中 / 发送失败,然后进行重试。在重试开始前,如果 revalidate 了数据,那么失败的内容会在 cache 中丢失。因此需要单独的客户端状态管理。
所以 react query 的文档方案不够用
设想要实现第五种方案。那么最终现实在屏幕上的。
有几种数据
- 服务器数据
- 客户端还未发送到服务端的数据
- 客户端已 / 正在发送的数据,正在到达客户端。
1.2. 没有重叠,但是是都与 3 有交叠。
整个数据流,从 2 开始,进入 3,然后 1 被更新。
这时候,3,1. 有交叠 <已发送,服务端更新的数据>
如果失败,3 中数据要回到 2
如果成功,3 中数据会和 1 有重叠
但是给到 list 的,应当是统一的,这有利于简化 ui 实现。否则数据在 2,3,1 的流转过程中,会造成 ui 上闪烁。
因此一个每个数据都应该是 type=<id,data,status>,但是客户端 id 怎么做?
这里隐含的一个问题是,当服务端更新数据之后,如何去重新增的服务端数据和发送成功的客户端数据。
因为对 item 进行更新时,这个过程,有一个响应机会,可以很好的建立客户端数据和服务端数据的关联。,因此对于一个 insert rpc,不应该只返回 ok 信息,而应该连带返回一些有区分度先重要信息,比如 id
这里是对 list 新增场景下的描述
那么,list 中某一项的更改怎么做?在这里,客户端盒服务端的数量是一致的,都有唯一 id,但是仍然有状态,updating 可能成功,也可能失败。这里可以通过 setquerydata,更新某一项内容,及其状态,通过 cache 进行处理。缺点在于仍不可轻易重试 (需要通过 mutation variable),判断 error
因此需要在客户端保存一个副本,指示 id,和目标状态以及当前状态。然后使用渲染时,每一项查询自己是否有待更新副本,根据这个数据进行重试,这里对 list 的修改其实本质上是对 item 到修改
然后就是 delete 的成功 / 失败,怎么做?客户端需要保有 deleting 的状态,id,status,先乐观更新,从 cache 中移除,成功删除状态标识,失败就将缓存恢复。最后 invalidquery
以上,都是非分页情况下,cud 的乐观更新
适用多组件复用,但本身数据量不大 (不分页)
单组件使用直接用 react 的 useOptimistic 也可以。(不共享状态)
// 竞态条件可以通过 mutate 时取消 query 进行
最后,分页情况下。如何处理?
比如,在页面末尾加上元素,筛选机制。
1. 新增数据位于分页数据的顶部
2. 新增数据位于分页数据的底部 / 中间。
3. 因为筛选规则的存在,新增数据不属于任何一页
分页的展示,无限列表,那么展示上乐观更新的数据不会有问题,新增 / 删除 / 更新
还有一种,在页面上展示分页数据,展示分页逻辑,那么乐观更新就会导致 page 的变动,从交互上,分页改变了 page,pagesize,这会很复杂
在无限列表中,分页的数据还有可能要处理虚拟列表以缓解性能压力
先看无限列表。
查询上,分页拆分到不同的请求上。
对于 reactquery,统一缓存在一个 useInfiniteQuery 中,然后用 page 去 map 到每一页
无限列表给出的 api 包括,load,has,isLoading 某个 page。
而 has 的判断需要由请求结果的 total,page 进行判断