你可能忽略了useSyncExternalStore + useOptimistic + useTransition

在现代前端应用中,实时数据更新和顺滑交互体验已经成了标配:

聊天室、协作文档、实时监控面板......都离不开实时通信乐观更新

关于乐观更新的定义:

它是一种用户界面(UI)更新策略,

在执行异步操作(例如网络请求、数据库写入)之前,
先假设操作一定会成功

直接更新 UI 显示预期结果,

再在请求返回时根据实际结果确认或回滚

React 18 引入了几个强大的新 Hook:

  • useSyncExternalStore:让组件安全订阅外部状态;
  • useOptimistic:实现用户操作的"即时反馈";
  • useTransition:让回滚或刷新变得平滑自然。

本文将通过一个实时聊天室的非完整案例,一步步带你理解这三者如何协同工作。

一、目标场景

我们要实现这样一个聊天室:

  1. 通过 WebSocket 接收服务器推送的消息;
  2. 用户输入后立即显示(不等服务端确认);
  3. 若发送失败,消息自动撤回或可重试。

看似简单,背后却包含了三种典型问题:

  • 外部状态同步(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 平滑撤销

六、应用场景

这种模式不仅能用于聊天室,还能应用在:

  • 实时评论区(评论先显示后确认)
  • 协作文档(本地编辑立即生效)
  • 实时监控(数据快速闪现)
  • 弹幕系统(先显示后同步)
  • 电商下单(先更新库存、后校验结果)

几乎所有实时 + 交互敏感的前端系统都能受益于这套组合。

相关推荐
parade岁月4 小时前
nuxt和vite使用环境比变量对比
前端·vite·nuxt.js
小帆聊前端4 小时前
Lodash 深度解读:前端数据处理的效率利器,从用法到原理全拆解
前端·javascript
Harriet嘉4 小时前
解决Chrome 140以上版本“此扩展程序不再受支持,因此已停用”问题 axure插件安装问题
前端·chrome
FuckPatience4 小时前
前端Vue 后端ASP.NET Core WebApi 本地调试交互过程
前端·vue.js·asp.net
Kingsdesigner4 小时前
从平面到“货架”:Illustrator与Substance Stager的包装设计可视化工作流
前端·平面·illustrator·设计师·substance 3d·平面设计·产品渲染
一枚前端小能手5 小时前
🔍 那些不为人知但是好用的JS小秘密
前端·javascript
屿小夏5 小时前
JSAR 开发环境配置与项目初始化全流程指南
前端
微辣而已5 小时前
next.js中实现缓存
前端
Dcc5 小时前
纯 css 实现前端主题切换+自定义方案
前端·css