useOptimistic
useOptimistic 允许在进行异步操作时显示一个临时state。它接受一个state输入,并通过一个 updateFn 来更新临时的state,此更新通常被成为乐观更新,待异步操作完成时触发set原state,原state 更新触发重渲染,React 会用新 state 重新执行 useOptimistic,此时乐观值被丢弃,UI 回到真实值。
使用场景
useOptimistic 用于“用户触发了一个会走异步/服务端的动作,但我希望 UI 立刻给出成功后的预期状态”,也就是先乐观展示、失败再回滚/提示,从而让交互更顺滑。
它适合“高成功率、可回滚”的操作;如果失败代价很大或强一致性要求高(比如扣款/下单库存),不建议纯乐观,至少要更谨慎的确认与补偿策略。 需要考虑失败处理:回滚、toast、重试、幂等(避免重复插入/重复点赞)等。
应用场景
- 表单提交/评论发送/聊天发消息:点击发送后马上把消息插入列表并标记 sending...,成功后去掉标记;失败则标红、显示重试按钮或撤回。
- 点赞/收藏/关注/投票:点一次立刻让计数 +1/按钮变已赞,后台请求完成后确认;失败则撤销并提示“操作失败,请重试”。
- 列表的增删改(本地先改列表,服务端后落库):例如待办事项“勾选完成/删除/新增”,先更新列表让用户看到变化,再等待接口返回。 轻量级设置切换:例如“开启通知/深色模式偏好同步到服务器”,先切 UI 开关状态,接口失败再恢复
import { useOptimistic, useState, useRef } from "react";
/**
* 当原 state 更新(比如 setMessages((prev) => [...prev, ...]))时,optimisticMessages 会自动更新吗?
*
* 会的。因为 optimisticMessages 不是独立存储,它是由 messages + optimistic 队列计算出来的派生结果。setMessages 触发重新渲染后,useOptimistic 会用新的 messages 重新计算 optimisticMessages。
*/
type Message = {
id: string;
text: string;
sending: boolean;
}
export async function deliverMessage(text: string) {
await new Promise((res) => setTimeout(res, 1000));
return text;
}
export default function UseOptimisticDemo1() {
const [messages, setMessages] = useState<Message[]>([]);
const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[], Message>(
messages,
(state, newMessage) => {
if (state.some((m) => m.id === newMessage.id)) return state;
return [...state, newMessage];
}
);
const form = useRef<HTMLFormElement>(null);
async function formAction(formData: FormData) {
const raw = formData.get("message");
if (typeof raw !== "string") return;
const text = raw.trim();
if (!text) return;
const optimisticMessage: Message = {
id: crypto.randomUUID(),
text,
sending: true,
};
addOptimisticMessage(optimisticMessage);
form.current?.reset();
const deliveredText = await deliverMessage(text);
setMessages((prev) => [...prev, { ...optimisticMessage, text: deliveredText, sending: false }]);
}
return (
<>
<form action={formAction} ref={form}>
<input
type="text"
name="message"
placeholder="enter your message"
/>
<button
type="submit"
style={{marginLeft: '10px'}}
>Send</button>
</form>
{optimisticMessages.map((message, index) => (
<div key={message.id}>
{message.text}
{!!message.sending && <small> (Sending...)</small>}
</div>
))}
</>
);
}