之前文章中阐述了使用useDeferredValue进行渲染时的优化,本篇文章即将讲解的useOptimisitic是从另外个维度同样做到了用户体验的优化~
首先需要告知下,useOptimisitic目前还是在试验中的特性 ,直接在项目中去使用它是有风险的。但是不妨碍我们学习它,因为它优化的角度可以让我们有新的视角去考虑项目中是否存在进一步优化的地方。
本篇文章会阐述以下几点:
- useOptimisitic的使用
- useOptimisitic如何进行渲染优化
- 如果不使用useOptimisitic,我们如何优化
一、使用useOptimisitic
首先我们需要给这个钩子下个定义,引用文档的话~
useOptimistic
is a React Hook that lets you optimistically update the UI.
通过这样的钩子我们可以乐观的更新视图。。。直译过来简直无法理解,😅
我们直接看代码如何书写:
js
const [optimisticState, addOptimistic] = useOptimistic(
someState,
// updateFn
(currentSomeState, optimisticValue) => {
// merge and return new state
// with optimistic value
}
);
简单来说,钩子调用后会吐出一个optimisticState 和addOptimistic,optimisticState可以理解为是一个someStated的中间态,试想下为什么我们需要得到一个中间态呢?
因为someState的更新有可能是延迟的,譬如请求接口后才完成状态的翻转,在状态更新前我们可以立马得到一个中间态的值,那么这个中间态等于什么呢?
获得的中间态值是开发者自定义出来的,也就是上面示例中的updateFn回调函数,开发者可以通过回调函数中的参数someState以及当前的中间态值去重新生成一个新的中间态值。
二、useOptimisitic进行渲染优化
通过上面的简单介绍,想必大家还是无法理解具体使用的场景,下面结合官网的案例向大家阐述。
假设我们现在有个发送消息的界面,用户输入input框中的内容,每次submit会将消息提交到后端,再将消息结果展示到页面上。请求到后端的时间被人为的变慢了,像这样的场景。我们就可以考虑优化~
优化的方案就是:我们将用户输入的消息先直接展示到页面中,等后端的请求完成的时候再进行修正。
从这点理解起来确实是乐观的更新视图,预判了正确请求响应的结果。描述完了方案,我们可以看看核心代码的书写。
这里需要注意,官网的案例中在更新状态有些bug,目前给React提了issue,大家可以参考我修正的版本:官网demo修正版
js
function Thread({ messages, sendMessage }) {
const formRef = useRef();
// 提交请求
async function formAction(formData) {
// 请求前获取中间态值 这样的值是携带sending标识 代表发送中
addOptimisticMessage(formData.get("message"));
formRef.current.reset();
// 发起请求
await sendMessage(formData);
}
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true
}
]
);
return (
<>
{optimisticMessages.map((message, index) => (
<div key={index}>
{message.text}
{!!message.sending && <small> (Sending...)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Hello!" />
<button type="submit">Send</button>
</form>
</>
);
}
核心的地方就在于useOptimistic的使用~
这里在提交表单发送请求前,会通过调用addOptimisticMessage去修改中间态的值,这样我们页面中间态值optimisticMessages就会立马得到更新,更新的内容就是携带输入的信息和sending:true的标识,如下回调函数:
js
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true
}
]
这样一来,用户就无须等待服务端响应就可以看到消息内容。
看到这里我们似乎有个疑问,这就像是自己伪造了下请求响应的内容用于展示而已,为什么非要用useOptimistic呢?
关键就是就在于当请求真的响应回来的时候,optimisticMessage会自动更新为最新的state值
大家可以在我贴出来的codeSand中演示就能发现,optimisticMessage被修正的过程。这里其实有个体验问题,如果我们输入的比较快的时候会发现同一条消息在界面中会出现两次,一个是携带sending、一个是没有sending。按道理应该是直接替换掉的,体验会比较好。
三、不使用useOptimistic进行渲染优化
看了以上的讲解,相信大家已经对这个试验中的hook一定的认识了,那么我们要立马使用它吗?最好是不要,还是等它稳定之后再使用吧🐶~
不过它提供了一个视角让我们面对这样的场景优化
通过中间态的值即时的去响应用户的界面,后续再进行修正。
所以,当我们不去使用这样的钩子,我们该如何实现呢?
整体的逻辑还是上述的思路,只不过我们需要自己维护和同步两套状态值,一套是后端响应的值,另一套是用户即时输入的值,并且当后端响应后,我们要去手动同步或修正它们。完整的案例:不使用useOptimistic版本
核心代码:
js
function Thread({ messages, sendMessage }) {
const formRef = useRef();
const [tempMessages, setTempMessages] = useState(messages);
useEffect(() => {
// 在副作用里 根据id去修正对应的messages的pending状态
const mergeTempMessages = tempMessages.map((info) => {
if (messages.find((item) => item.id === info.id)) {
return {
...info,
isPending: false
};
}
return info;
});
setTempMessages(mergeTempMessages);
}, [messages]);
async function formAction(formData) {
let newId = id++;
setTempMessages((tempMessages) => {
return [
...tempMessages,
{ id: newId, text: formData.get("message"), isPending: true }
];
});
formRef.current.reset();
sendMessage(formData, newId);
}
return (
<>
{tempMessages.map((message, index) => {
return (
<div key={index}>
{message.text}
{!!message.isPending && <small> (Sending...)</small>}
</div>
);
})}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Hello!" />
<button type="submit">Send</button>
</form>
</>
);
}
核心就在于我们使用了useEffect去同步和修正tempMessages的值 ,让它和请求响应回来的值进行同步。另一个关键点就是如何找到对应的messages值进行同步,所以需要维护自增id,用于标识对应的消息体。
这样一来同样能够实现渲染的优化,只不过些许有点麻烦,没有直接使用钩子方便。但是,大家可以在codeSand中演示就会发现这样的方案页面中同一条消息就不会出现两次了。
四、总结
useOptimisitic本质上解决的是数据响应的问题,它允许开发者自定义中间态的值,快速去响应用户的交互,等接口响应之后再同步中间态值。
所以开发者要去甄别使用的场景,如果你能乐观的预判数据的值,那么就可以采用此方案。
由于是试验中的能力,所以不建议大家直接使用,但是它解决问题的视觉值得我们去采纳,我们可以不使用新特性去达到一样的效果。
如果大家有想了解的Hook解析或者疑问,欢迎留言讨论或私信。如果大家对源码感兴趣,可以移步到另外一个专栏:渐进式解剖React