浅谈React19的破坏性更新

2024年12月,React 19 正式发布,至今已过去大半年。尽管目前多数项目仍在使用 React 18,但我们可以通过官方文档和 GitHub 了解其带来的关键新特性和破坏性变更。以下是我总结的一些重要更新。

React相关文档也可参考这个地址:传送门

1、Optimistic (直译为:乐观)

React19新加入了一个概念,叫做乐观 。对应的API为useOptimistic。乐观更新是一种 UI 设计模式,其核心思想是:

"先相信操作会成功,提前更新 UI;如果失败了,再回滚到之前的状态。 这与传统的"悲观更新"(先等待服务器响应,成功后再更新 UI)形成对比。

为什么需要乐观更新?
  • 提升用户体验:用户点击按钮后,UI 立即响应,无需等待网络延迟。
  • 感觉更快:即使网络慢,用户也能看到操作"已生效",减少等待焦虑。
  • 现代应用的标准做法:如社交媒体点赞、评论删除等,都采用此模式。

示例

js 复制代码
import { useOptimistic, useState } from "react";
import { postComment as apiPostComment } from "../utils";

const apiPostComment = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('comment success')
    }, 1000)
  })
}

// 评论项组件
function CommentItem({ comment }) {
  return (
    <div className="border p-3 mb-2 rounded bg-white shadow-sm">
      <p className="text-gray-800">{comment.text}</p>
      {comment.isPending && (
        <small className="text-blue-500 mt-1 block">正在提交...</small>
      )}
    </div>
  );
}

// 评论区域组件
function CommentSection({ commentList, onAddComment }) {
  const [pendingCommentId, setPendingCommentId] = useState(0);

  // 乐观更新:立即显示待定评论
  const [displayedComments, addPendingComment] = useOptimistic(
    commentList,
    (state, newCommentText) => {
      const id = `pending-${pendingCommentId}`;
      return [
        ...state,
        {
          id,
          text: newCommentText,
          isPending: true,
        },
      ];
    }
  );

  // 表单提交处理器
  async function handleAddComment(formData) {
    const content = formData.get("commentContent");
    if (!content?.trim()) return;

    // 生成本次提交的临时 ID
    const currentPendingId = pendingCommentId;
    setPendingCommentId((prev) => prev + 1);

    // 1. 立即乐观更新 UI
    addPendingComment(content);

    try {
      // 2. 发起真实请求
      const result = await onAddComment(formData, `pending-${currentPendingId}`);

      if (result.error) {
        throw new Error(result.error.message);
      }
    } catch (error) {
      console.error("评论提交失败:", error.message);
      // 注意:useOptimistic 不会自动回滚,需配合其他状态管理
    }
  }

  return (
    <div className="comment-container">
      {/* 渲染当前显示的评论(含乐观更新) */}
      {displayedComments.length === 0 ? (
        <p className="text-gray-500 italic">暂无评论</p>
      ) : (
        displayedComments.map((comment) => (
          <CommentItem key={comment.id} comment={comment} />
        ))
      )}

      {/* 添加评论表单 */}
      <form onSubmit={handleAddComment} className="mt-4 space-y-3">
        <textarea
          name="commentContent"
          placeholder="写下你的评论..."
          rows="3"
          className="w-full border rounded p-2 focus:outline-none focus:ring-2 focus:ring-blue-300"
          autoComplete="off"
        />
        <button
          type="submit"
          className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded transition"
        >
          发布评论
        </button>
      </form>
    </div>
  );
}

// 页面组件:评论功能演示
export default function OptimisticCommentDemo() {
  const [errorMessage, setErrorMessage] = useState("");
  const [comments, setComments] = useState([
    { id: "initial-1", text: "第一条评论!", isPending: false },
  ]);

  // 处理真实评论提交
  async function handleCommentSubmit(formData, pendingId) {
    const content = formData.get("commentContent");

    // 模拟 API 调用
    const response = await apiPostComment({ content });

    if (response.error) {
      setErrorMessage(`"${content}" 提交失败:${response.error.message}`);
      return response;
    } else {
      setErrorMessage("");
      // ✅ 成功后:添加真实评论(不带 isPending)
      setComments((prev) => [
        ...prev,
        {
          id: `comment-${Date.now()}`,
          text: response.data.text,
          isPending: false,
        },
      ]);
    }

    return response;
  }

  return (
    <div className="p-6 max-w-2xl mx-auto">
      <h2 className="text-2xl font-bold text-gray-800 mb-4">
        乐观更新评论系统演示
      </h2>

      <CommentSection commentList={comments} onAddComment={handleCommentSubmit} />

      {errorMessage && (
        <p className="text-red-600 mt-4 text-sm bg-red-50 p-3 rounded">
          {errorMessage}
        </p>
      )}
    </div>
  );
}

2、use (支持异步)

1、use可读取promise

示例:

js 复制代码
import { useState, use, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

// 模拟异步获取天气数据
function fetchWeatherData(city) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 50% 概率失败,增加错误边界演示效果
      if (Math.random() < 0.5) {
        reject(new Error("天气服务暂时不可用"));
      } else {
        resolve({
          city,
          temperature: Math.round(Math.random() * 30),
          condition: ["晴", "多云", "小雨", "雷阵雨"][Math.floor(Math.random() * 4)],
          lastUpdated: new Date().toLocaleTimeString(),
        });
      }
    }, 1200);
  });
}

export default function WeatherDashboard() {
  const [weatherQuery, setWeatherQuery] = useState(null);
  const [isFetching, setFetching] = useState(false);

  const handleFetchWeather = () => {
    setWeatherQuery(fetchWeatherData("杭州"));
    setFetching(true);
  };

  if (!isFetching) {
    return (
      <div className="text-center">
        <h2 className="text-xl font-semibold mb-4">🌤️ 天气信息看板</h2>
        <button
          onClick={handleFetchWeather}
          className="bg-green-500 hover:bg-green-600 text-white px-5 py-2 rounded transition"
        >
          获取杭州天气
        </button>
      </div>
    );
  }

  return <WeatherDisplay weatherPromise={weatherQuery} />;
}

// 展示天气数据的容器(含错误和加载状态)
function WeatherDisplay({ weatherPromise }) {
  console.log(
    "%c [ WeatherDisplay ]-38",
    "font-size:13px; background:#4B5563; color:#fff; padding:2px 6px;",
    "渲染 WeatherDisplay"
  );

  return (
    <div className="max-w-md mx-auto p-4 border rounded-lg shadow bg-white">
      <h3 className="text-lg font-medium text-gray-800 mb-3">🌤️ 实时天气</h3>

      <ErrorBoundary fallback={<WeatherError />}>
        <Suspense fallback={<WeatherLoading />}>
          <WeatherCard dataPromise={weatherPromise} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

// 加载中状态
function WeatherLoading() {
  return (
    <div className="text-center text-gray-500 animate-pulse">
      <p>📡 正在连接天气服务器...</p>
      <p className="text-sm mt-1">请稍候</p>
    </div>
  );
}

// 错误状态
function WeatherError() {
  return (
    <div className="text-center text-red-600">
      <p>❌ 获取天气失败</p>
      <p className="text-sm mt-1">请检查网络或稍后重试</p>
    </div>
  );
}

// 实际渲染天气数据(使用 use)
function WeatherCard({ dataPromise }) {
  const weather = use(dataPromise);

  return (
    <div>
      <p><strong>城市:</strong>{weather.city}</p>
      <p><strong>温度:</strong>{weather.temperature}°C</p>
      <p><strong>天气:</strong>{weather.condition}</p>
      <p className="text-xs text-gray-500 mt-2">
        更新时间:{weather.lastUpdated}
      </p>
    </div>
  );
}

2、use可读取context

示例:

js 复制代码
import {use} from 'react';
import ThemeContext from './ThemeContext'

function Heading({children}) {
  if (children == null) {
    return null;
  }
  
  const theme = use(ThemeContext);
  return (
    <h1 style={{color: theme.color}}>
      {children}
    </h1>
  );

3、Ref 支持在 props 中转发使用

从19开始,ref可以作为prop在函数组件中使用了。之前函数组件中想要使用ref,必须使用forwardRef。这表示着从React19开始,forwardRef可能要被弃用了。会对一些三方组件库有比较大的影响,因为基本上每个组件都会用到ref,之前的写法都是使用的forwardRef

1、React19给ref加上了自己的清理函数,之前可以用useEffect清理,现在ref有自己的清理函数了。

示例:

js 复制代码
<div
  ref={(ref) => {
    // 当元素从DOM中移除时的引用。
    return () => {
      // ref 清理函数
    };
  }}
/>

4、Context 改动

之前使用 Context 基本就是三步走:创建Context;Provider传递value;后代组件消费value。React19中简化了Context.Provider,可以直接使用Context代替Provider:

示例:

js 复制代码
const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );  
}

5、其他改动

1、支持文档元数据

示例:

js 复制代码
export default function Demo ({ props }) {
  return (
    <section>
      <title>{props.title}</title>
      <meta name="name" content="content" />
      <Link rel="test" href="xxx" />
      <meta name="keywords" content={props.keywords} />
      <p>测试内容...</p>
    </section>
  )
}

2、支持样式表

js 复制代码
export default function Demo ({ props }) {
  return (
    <section>
      <link rel="stylesheet" href="xxx" />
      <p>测试内容...</p>
    </section>
  )
}

3、支持异步脚本

js 复制代码
export default function Demo ({ props }) {
  return (
    <section>
      <script async={true} src="xxx" />
      <p>测试内容...</p>
    </section>
  )
}

6、支持自定义元素

在之前的版本中,React会把不认识的props当做attributes来处理,在React19中,此行为改为与自定义元素实例上的属性匹配的 props 被分配为 properties,其他的被分配为attributes。

相关推荐
温宇飞3 小时前
Web 异步编程
前端
东华帝君3 小时前
react组件常见的性能优化
前端
第七种黄昏3 小时前
【前端高频面试题】深入浏览器渲染原理:从输入 URL 到页面绘制的完整流程解析
前端·面试·职场和发展
angelQ3 小时前
前端fetch手动解析SSE消息体,字符串双引号去除不掉的问题定位
前端·javascript
Huangyi3 小时前
第一节:Flow的基础知识
android·前端·kotlin
林希_Rachel_傻希希3 小时前
JavaScript 解构赋值详解,一文通其意。
前端·javascript
Yeats_Liao3 小时前
Go Web 编程快速入门 02 - 认识 net/http 与 Handler 接口
前端·http·golang
金梦人生3 小时前
🔥Knife4j vs Swagger:Node.js 开发者的API文档革命!
前端·node.js
东华帝君3 小时前
react 虚拟滚动列表的实现 —— 固定高度
前端