React19新特性,乐观更新useOptimistic()范例代码优化
发布于 作者:苏南大叔 来源:程序如此灵动~乐观更新这个事情,其实存在着一些问题。主要体现在因为乐观更新,就不考虑服务器提交等待这个问题。界面上的各种按钮及输入框,是没有disabled
这种说法的。想想看,这种提交等待的传统做法,是不是和乐观更新的思想冲突呢?短时间内大量提交的话,消息的时间轴存在混乱和重复的问题。
苏南大叔的“程序如此灵动”博客,记录苏南大叔的代码编程经验总结。本文测试环境:nodejs@20.18.0
,create-react-app@5.0.1
,react@19.0.0
。本文视图解决useOptimistic()
官方范例里面的消息的时间轴重复和混乱的问题。
前文回顾
本文代码的useOptimistic()
范例代码,来自下面的这篇文章。
主要实现的功能就是:面对大概率成功的接口数据交互,提前假设数据正常返回,界面迅速得到渲染,提高用户体验。这个问题,被乱蒿羊毛的结果,就是极短时间内多次触发提交的话。数据混乱和重复的问题。
useTransition()
试图通过useTransition()
来解决上述问题,效果也并不明显。参考文章:
使用useTransition()
,来获得pending
状态和一个可过渡的逻辑包装组合。被useTransition()
包裹的代码端,具有较低的优先级,如果有界面交互用户输入的高优先级任务的时候,这些低优先级的任务会被延迟执行。
考虑到和乐观更新的效果相抵触,代码中并没有对pending
状态进行使用。仅仅保留了formAction
中的startTransition()
。
import { useOptimistic, useTransition } from "react";
const [isPending, startTransition] = useTransition();
async function formAction(formData) {
startTransition(async () => {
//...
});
}
其实这个useTransition()
效果,并不明显。似乎更多的是出于心理安慰的作用。
增加消息id
这里对每条消息增加了一个key
,其实也可以理解为自增id
。主要想利用这个自增值来确定消息的身份和先后顺序。
let message_id = useRef(messages.length);
async function formAction(formData) {
startTransition(async () => {
let message = {
text: formData.get("message"),
key: message_id.current.toString(),
};
message_id.current++;
//...
});
}
解决顺序混乱的问题
顺序为什么会混乱,主要原因在于苏南大叔修改的代码里面,服务器端返回的时间做了随机,这也更贴近现实的情况。如果服务器返回时间都一致的话,消息顺序并不会“混乱”。
解决方案的原理,苏南大叔是利用界面格式化的时候,对"乐观数据"进行了排序(`.sort())。
{optimisticMessages
.sort((a, b) => a.key - b.key)
.map((message, index) => (
<div key={index}>
{message.key}.{message.text}
{!!message.sending && <small>(发送中……)</small>}
</div>
))}
解决数据重复的问题
据仔细观察,重复的数据都是瞬时存在的。两条同一个key
的数据,处于两个不同的sending
状态。如果调试useOptimistic()
钩子的第二个参数的话。会发现这个合并数据的逻辑被执行了两次。所以,问题就出在这个“两次”上。
修改完的逻辑如下:
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => {
if (state.some((a) => a.key == newMessage.key)) {
return state;
}
return [
...state,
{
key: newMessage.key,
text: newMessage.text,
sending: true,
},
];
}
);
完整代码
完整代码如下:
import { useOptimistic, useTransition, useState, useRef } from "react";
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function serverAPI(message) {
let tmp = getRandomInt(1, 8) * 1000;
await new Promise((res) => setTimeout(res, tmp));
return message; // 这个return很重要,被合并到真实state里面,进而影响乐观state
}
function Main({ messages, addRealMessageFn }) {
const [isPending, startTransition] = useTransition();
const formRef = useRef(null);
let message_id = useRef(messages.length);
async function formAction(formData) {
startTransition(async () => {
let message = {
text: formData.get("message"),
key: message_id.current.toString(),
};
message_id.current++;
addOptimisticMessage(message);
formRef.current.reset();
await addRealMessageFn(message);
});
}
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => {
if (state.some((a) => a.key == newMessage.key)) {
return state;
}
return [
...state,
{
key: newMessage.key,
text: newMessage.text,
sending: true,
},
];
}
);
return (
<>
<form action={formAction} ref={formRef}>
<input
type="text"
name="message"
placeholder="你好!"
defaultValue={"sunan"}
/>
<button type="submit">发送</button>
</form>
{optimisticMessages
.sort((a, b) => a.key - b.key)
.map((message, index) => (
<div key={index}>
{message.key}.{message.text}
{!!message.sending && <small>(发送中……)</small>}
</div>
))}
</>
);
}
export default function App() {
const [messages, setMessages] = useState([
{ text: "你好,苏南大叔", key: 0 },
{ text: "hola,Uncle Sunan", key: 1 },
]);
async function addRealMessageFn(message) {
const okMessage = await serverAPI(message);
setMessages((messages) => [
...messages,
{ text: okMessage.text, key: okMessage.key },
]);
}
return <Main messages={messages} addRealMessageFn={addRealMessageFn} />;
}
结语
在本文的代码里面,对于“乐观更新”这个思想,又增加了新的认识。更新可以保持乐观,但是也带来了一定的数据不可靠性。所以,双刃剑。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。