在现代前端应用中,实时数据更新和顺滑交互体验已经成了标配:
聊天室、协作文档、实时监控面板......都离不开实时通信 与乐观更新。
关于乐观更新的定义:
它是一种用户界面(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 平滑撤销
        六、应用场景
这种模式不仅能用于聊天室,还能应用在:
- 实时评论区(评论先显示后确认)
 - 协作文档(本地编辑立即生效)
 - 实时监控(数据快速闪现)
 - 弹幕系统(先显示后同步)
 - 电商下单(先更新库存、后校验结果)
 
几乎所有实时 + 交互敏感的前端系统都能受益于这套组合。