在现代前端应用中,实时数据更新和顺滑交互体验已经成了标配:
聊天室、协作文档、实时监控面板......都离不开实时通信 与乐观更新。
关于乐观更新的定义:
它是一种用户界面(UI)更新策略,
在执行异步操作(例如网络请求、数据库写入)之前,
先假设操作一定会成功 ,直接更新 UI 显示预期结果,
再在请求返回时根据实际结果确认或回滚。
React 18 引入了几个强大的新 Hook:
useSyncExternalStore
:让组件安全订阅外部状态;useOptimistic
:实现用户操作的"即时反馈";useTransition
:让回滚或刷新变得平滑自然。
本文将通过一个实时聊天室的非完整案例,一步步带你理解这三者如何协同工作。
一、目标场景
我们要实现这样一个聊天室:
- 通过 WebSocket 接收服务器推送的消息;
- 用户输入后立即显示(不等服务端确认);
- 若发送失败,消息自动撤回或可重试。
看似简单,背后却包含了三种典型问题:
- 外部状态同步(WebSocket);
- 乐观 UI(即时显示用户操作);
- 状态一致性与回滚(失败时撤回)。
二、第一步:用 useSyncExternalStore 管理实时状态
传统写法往往这样:
ini
useEffect(() => {
const ws = new WebSocket('ws://...');
ws.onmessage = e => setMessages(JSON.parse(e.data));
}, []);
这没问题,但有几个隐患:
- 多组件同时订阅时,可能重复连接;
- React 并发模式下可能产生状态不同步;
- 无法保证快照一致性。
React 官方提供的 useSyncExternalStore
专门解决这些问题。
实现 WebSocket Store
ini
// websocketStore.js
let socket = null;
let messages = [];
const listeners = new Set();
function notify() {
for (const listener of listeners) listener();
}
export function createWebSocketStore(url) {
if (socket) return; // 避免重复连接
socket = new WebSocket(url);
socket.onmessage = (e) => {
const data = JSON.parse(e.data);
messages = [...messages, data];
notify(); // 通知所有订阅组件更新
};
socket.onopen = () => console.log("✅ connected");
socket.onclose = () => console.log("❌ closed");
}
export const store = {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot() {
return messages;
},
};
// 模拟发送函数
export function sendMessage(msg) {
if (socket?.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(msg));
}
}
这段代码就是一个可订阅的全局状态容器 ,
React 组件只需订阅它,就能安全地读取 WebSocket 状态。
三、第二步:用 useOptimistic 实现"消息先显示"
我们希望用户点击"发送"后,消息立刻显示在 UI 上,而不是等到服务器回应。
useOptimistic
就是为这种"乐观更新"场景设计的。
实现示例:
javascript
import { useEffect, useSyncExternalStore, useOptimistic } from "react";
import { store, createWebSocketStore, sendMessage } from "./websocketStore";
export default function ChatApp() {
useEffect(() => {
createWebSocketStore("wss://example.com/chat");
}, []);
const messages = useSyncExternalStore(store.subscribe, store.getSnapshot);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(currentMessages, newMsg) => [...currentMessages, { ...newMsg, optimistic: true }]
);
function handleSend(text) {
const tempMsg = { user: "Asen", text, tempId: Date.now() };
addOptimisticMessage(tempMsg);
sendMessage(tempMsg);
}
return (
<div className="p-4 max-w-md mx-auto">
<h2 className="font-bold mb-2 text-lg">💬 实时聊天室</h2>
<MessageList messages={optimisticMessages} />
<ChatInput onSend={handleSend} />
</div>
);
}
function MessageList({ messages }) {
return (
<ul className="space-y-1">
{messages.map((msg, i) => (
<li
key={msg.tempId || i}
className={`p-2 rounded ${
msg.optimistic ? "bg-gray-200 text-gray-500 italic" : "bg-blue-100"
}`}
>
{msg.user}: {msg.text}
{msg.optimistic && " (sending...)"}
</li>
))}
</ul>
);
}
现在,用户每次发消息都会立刻在屏幕上出现 (sending...)
,
真正的服务器消息到达后再被替换掉。
四、第三步:加入 useTransition 实现"失败回滚"
接下来,我们要让发送失败的消息自动撤回。
在实际项目中,这非常常见:网络波动、服务端延迟、权限问题等等。
修改 sendMessage
javascript
// websocketStore.js
export function sendMessage(msg) {
return new Promise((resolve, reject) => {
if (socket?.readyState !== WebSocket.OPEN) {
reject("Socket not connected");
return;
}
// 模拟 30% 发送失败
setTimeout(() => {
if (Math.random() < 0.3) {
reject("Network error");
} else {
socket.send(JSON.stringify(msg));
resolve();
}
}, 400);
});
}
在组件中使用 useTransition 平滑回滚
javascript
import React, { useTransition } from "react";
const [isPending, startTransition] = useTransition();
async function handleSend(text) {
const tempId = Date.now();
const optimisticMsg = { user: "Asen", text, tempId, optimistic: true };
addOptimisticMessage(optimisticMsg);
try {
await sendMessage({ user: "Asen", text });
} catch (err) {
console.error("❌ Failed:", err);
startTransition(() => {
addOptimisticMessage((msgs) =>
msgs.filter((m) => m.tempId !== tempId)
);
});
}
}
这样做的效果:
- 消息先显示;
- 如果失败,React 平滑地将其移除;
- 整个过程无闪烁,无状态错乱。
五、三者协同背后的逻辑
Hook | 作用 | 在本例中表现 |
---|---|---|
useSyncExternalStore |
安全订阅外部数据源 | 从 WebSocket 获取实时消息 |
useOptimistic |
管理乐观状态(立即反馈) | 发送时立即显示临时消息 |
useTransition |
平滑更新、避免卡顿 | 回滚失败消息时保持流畅 |
三者组合形成一个非常稳定的结构:
markdown
用户操作 → useOptimistic (添加临时UI)
↓
真实异步操作 → 成功 → useSyncExternalStore 更新
↳ 回滚 → useTransition 平滑撤销
六、应用场景
这种模式不仅能用于聊天室,还能应用在:
- 实时评论区(评论先显示后确认)
- 协作文档(本地编辑立即生效)
- 实时监控(数据快速闪现)
- 弹幕系统(先显示后同步)
- 电商下单(先更新库存、后校验结果)
几乎所有实时 + 交互敏感的前端系统都能受益于这套组合。