一文搞懂——React 19到底更新了什么

前言

在2024年12月5日 React19正式版在npm上发布

一、性能提升进入自动化时代------核心更新

告别useMemo、useCallback、React.memo

在React18及以前,为了性能,你得小心翼翼地给函数和计算结果包上 useMemouseCallback,还要盯着那个烦人的依赖数组,写错一个就出 Bug。

但是在React19推出了 React Compiler。它就像一个聪明的助手,在后台自动帮你分析哪些组件需要缓存。你只需要写正常的 JS 代码,性能优化交给编译器,代码量瞬间少了一大截。

下面我们可以看一个例子------

React18的写法

jsx 复制代码
function ProductList({ products, filterTerm }) {
  // 1. 必须手动包裹 useMemo,否则每次渲染都会重新计算
  // 2. 必须死盯着这个 [products, filterTerm] 依赖数组,漏写一个就出 Bug
  const filteredProducts = useMemo(() => {
    console.log("正在执行繁重的过滤逻辑...");
    return products.filter(p => p.name.includes(filterTerm));
  }, [products, filterTerm]); 

  // 3. 传给子组件的函数,必须包裹 useCallback,否则子组件会跟着瞎重绘
  const handlePurchase = useCallback((id) => {
    console.log("购买商品:", id);
  }, []);

  return (
    <ul>
      {filteredProducts.map(p => (
        <Item key={p.id} product={p} onPurchase={handlePurchase} />
      ))}
    </ul>
  );
}

React19的写法

jsx 复制代码
function ProductList({ products, filterTerm }) {
  // 没有任何 Hook,就是纯 JS 逻辑
  // React Compiler 会在编译时自动分析:
  // "只要 products 和 filterTerm 没变,我就直接给上次的结果"
  const filteredProducts = products.filter(p => p.name.includes(filterTerm));

  // 这里的函数也不需要 useCallback
  // 编译器会自动帮你做"函数持久化",确保子组件不会因为引用变化而重绘
  const handlePurchase = (id) => {
    console.log("购买商品:", id);
  };

  return (
    <ul>
      {filteredProducts.map(p => (
        <Item key={p.id} product={p} onPurchase={handlePurchase} />
      ))}
    </ul>
  );
}

React Compiler的意义

React Compiler的意义远远不止省略了useMemo、useCallback、React.memo. 这是react19更新的核心理念的体现 "通过编译器的力量,抹平框架与原生语言之间的缝隙。"

React 19 的伟大之处不在于它增加了多少新 API,而在于它让'React 开发者'重新变回了'JavaScript 开发者'。它承认了人类的大脑不该用来记忆依赖数组,而该用来构建业务逻辑。

对比Vue

谈到React,Vue总是不可避免的一个话题,Vue在缓存方面是怎么做的呢?

Vue 写法

ini 复制代码
const count = ref(0);

// 你必须明确调用 computed 函数
// 虽然不用写依赖数组,但必须通过 .value 读值
const doubled = computed(() => count.value * 2);

React 18 写法

ini 复制代码
const [count, setCount] = useState(0);

// 1. 必须手动写 useMemo
// 2. 必须手动维护依赖数组 [count]
const doubled = useMemo(() => {
  return count * 2;
}, [count]);

React 19 写法

ini 复制代码
const [count, setCount] = useState(0);

// 没有任何框架 API,没有任何包裹函数
// 就像写普通的 JavaScript 一样
const doubled = count * 2;

我们可以看到,在React18之前,Vue的自动依赖追踪是很有优势的,但是在React19,react实现了从落后到反超,那么Vue什么时候会抛弃掉计算属性,也实现自动化优化呢?

React 19 通过"降维打击",把 Vue 曾经最引以为傲的"自动依赖追踪"变得更透明。

二、React19 的Actions机制------异步状态的"原生化"

useActionState

react18的Actions

在react18假设我们要写一个简单的评论框,逻辑是:点击提交 -> 显示加载中 -> 成功后清空输入框 -> 失败显示错误。那么经典的写法是下面这种

jsx 复制代码
function CommentForm() {
  // 痛点 1:必须手动定义一大堆状态
  const [comment, setComment] = useState("");
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // 痛点 2:手动开启 Loading
    setIsPending(true);
    setError(null);

    try {
      await saveComment(comment); // 异步请求
      setComment(""); // 成功后手动重置
    } catch (e) {
      // 痛点 3:手动捕获并处理错误
      setError(e.message);
    } finally {
      // 痛点 4:手动关闭 Loading
      setIsPending(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <textarea 
        value={comment} 
        onChange={(e) => setComment(e.target.value)} 
      />
      {/* 痛点 5:Loading 状态分散在各处,容易漏掉禁用逻辑 */}
      <button disabled={isPending}>
        {isPending ? "提交中..." : "发布评论"}
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

可能有小伙伴不理解为什么一定要定义状态isPending和error,我们直接使用不行么?

我们要知道的是react的核心思想是UI=f(date),数据(状态)是驱动UI渲染的最终因素,底层的DOM批量更新也是通过判断状态是否变化来确定的。如果不设置isPending和error就可能出现下面的情况------

你发起一个异步请求,即使后台正在疯狂下载数据,React 的渲染引擎根本不知道这件事。

用户点击了"提交",页面却没有任何反应(按钮没变灰、没有转圈圈)。用户以为没点着,就会不停地狂点,导致后台收到一堆重复请求。

所以存储 isPending 是为了告诉 React:我现在状态变了,请你赶紧重绘一下 UI。

react18中我们设置的状态到底在做什么

在 React 18 的这种模式下,开发者实际上在充当 "状态协调员"

竞态条件:如果用户连点两次按钮,你需要写额外的逻辑防止第二次请求覆盖第一次。

UI 不同步:如果你忘了在 finally 里关闭 isPending,按钮就永远锁死了。

样板代码冗余:每一个表单、每一个按钮点击,你都要重复写这套 try-catch-finally。

在 React 18 中,异步操作像是一头需要你时刻盯着的猛兽,你必须手动给它关笼子(setLoading)、喂食(handleError)。而在 React 19 中,React 给异步操作装上了'自动驾驶系统'。你只需要告诉它目的地(Action 函数),它会自动处理起步、巡航和刹车。

react19的优化

在react19中的写法是这样的

jsx 复制代码
function CommentForm() {
  // state 包含返回结果,formAction 是触发函数,isPending 是自动追踪的状态
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      const content = formData.get("comment");
      const result = await saveComment(content); 
      return result; // 返回的结果会自动存入 state
    },
    null
  );

  return (
    // 注意:这里不再用 onSubmit,而是用 action
    <form action={formAction}>
      <textarea name="comment" />
      
      {/* isPending 由 React 自动管理,Promise 没结束它就是 true */}
      <button disabled={isPending}>
        {isPending ? "提交中..." : "发布评论"}
      </button>
      
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

我们可以观察到

  • 消失了: const [loading, setLoading] = useState(false)。

  • 消失了: try { ... } finally { setLoading(false) }。

  • 消失了: 输入框的 value 和 onChange(利用 formData 直接读取)。

  • 消失了: 对"请求是否结束"的手动判断。

为什么以前我们总是在写重复的 useState(false)?因为 React 18 只负责渲染,不负责跟踪你的异步任务。而 React 19 的 Actions 机制,让框架开始'理解'异步任务的生命周期,从而把开发者从重复的状态定义中解放出来。

useOptimistic

useOptimistic主要解决乐观更新的问题。

react18的乐观更新

javascript 复制代码
function LikeButton() {
  const [likes, setLikes] = useState(100); // 真实数据
  const [error, setError] = useState(null);

  const handleLike = async () => {
    // 1. 备份旧数据(为了失败时回滚)
    const previousLikes = likes;

    // 2. 立即更新 UI(乐观更新)
    setLikes(likes + 1);

    try {
      await updateLikeApi(); // 发送请求
    } catch (e) {
      // 3. 失败处理:手动把数据改回去
      setLikes(previousLikes); 
      setError("更新失败,已回滚");
    }
  };

  return (
    <button onClick={handleLike}>
      {likes} 👍 {error && <span>{error}</span>}
    </button>
  );
}

react19的乐观更新

javascript 复制代码
import { useOptimistic } from 'react';

function LikeButton({ initialLikes }) {
  // 1. 它是基于原始数据的"派生状态"
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, newLike) => state + 1 // 定义如何"乐观地"改变
  );

  async function handleLike() {
    // 2. 瞬间改变 UI
    addOptimisticLike(1);

    // 3. 发送真实请求(配合 Actions)
    await updateLikeApi();
    
    // 【关键点】:函数执行完,React 自动把 UI 同步为服务器回来的真实 initialLikes
    // 开发者不需要写任何"回滚"代码!
  }

  return (
    <button onClick={handleLike}>{optimisticLikes} 👍</button>
  );
}

useOptimistic 引入了一种 '阅后即焚'的状态管理模式。它不再需要开发者去写复杂的同步逻辑,而是通过监听异步函数的'生命周期',自动完成了从'幻想'到'现实'的平滑切换。这种绑定不是通过代码硬连的,而是通过时间维度(异步执行的过程) 自动关联的。

三、ref 不再需要"中间商":再见 forwardRef

在 React 18 时代,如果你想把 ref 传递给子组件,你必须使用 forwardRef 包裹子组件。这是 React 中最令人反感的"模板代码"之一,写法极其拗口。

React 18 的痛苦写法

JavaScript 复制代码
const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

React 19 的简洁写法: ref 现在可以像 id、className 一样作为普通的 prop 传递。

JavaScript 复制代码
function MyInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

这一改动抹平了 ref 与普通属性之间的差异,组件的结构变得更加扁平、直观。

为什么之前的设计要求使用forwardRef?

1. 语义冲突:谁才是 ref 的主人?

在 React 的早期设计中,ref 的语义非常霸道:它永远指向"当前标签"所对应的直接实例。

  • 对于原生标签 (如 <input />):ref 指向真实的 DOM 节点。

  • 对于组件标签 (如 <MyComponent />):ref 指向该组件的实例(Instance)。

如果没有 forwardRef,会发生什么? 假设你写了 <MyInput ref={myRef} />

  1. React 引擎会认为:你想拿到 MyInput 这个组件包装盒的引用。

  2. 如果 ref 混在 props 里,子组件内部又把这个 ref 传给了底层的 。

  3. 此时系统就乱了:同一个 ref 到底该指向外层的组件实例,还是里层的 DOM 节点?

为了避免这种指向不明的混乱,React 强制规定:ref 不属于 props,它必须由框架层统一调度。

2. 类组件时代的"实例保护"

在 Hooks 出现之前,React 主要是类组件。

类组件有内部状态、有方法(比如 this.myMethod())。通过 ref 拿到组件实例是非常强大的功能。

如果 React 允许 ref 随 props 传递,子组件可能会不小心修改或覆盖掉父组件传下来的 ref。

forwardRef很大程度上是一份"免责声明"React 官方当时觉得:"转发 Ref 是一件很危险的操作,不能让你随随便便就做了。"于是它强制要求你写 forwardRef。这个复杂的语法实际上是在逼你确认:"我,开发者,现在明确知道我要把父组件的控制权(Ref)交给子组件处理了。如果子组件乱搞,或者指向出错了,我认了。"

将 ref 抽离出来,通过特殊的 forwardRef 接口,实际上是在强迫开发者意识到:"注意,你现在正在把原本属于组件外部的控制权,穿透到组件内部。" 这是一种显式的声明,防止开发者无意中破坏了封装性。

3. 性能与一致性

React 的底层有一个非常高效的 props 比较机制。

props 通常是不可变的纯数据。

ref 是一个可变对象,其 current 属性会随生命周期不断变化。

如果把这个"多动"的 ref 塞进 props,会导致 React 在判断组件是否需要重渲染时,不得不对 ref 做特殊逻辑判断,增加了底层的复杂度。

4.为什么 React 19 现在又敢合回去了?

React 19 之所以能取消 forwardRef,是因为两个环境变了:

函数组件成为绝对主流:函数组件没有"实例"。这意味着当你写 <MyComponent ref={myRef} /> 时,除非你手动转发,否则这个 ref 根本没东西可绑。既然没东西绑,那就不存在"语义冲突"了

编译器变聪明了:React Compiler 现在可以精准地追踪 ref 的去向。既然工具能搞定,就没必要让程序员手写那一层蹩脚的包裹函数了。

forwardRef 的消失,本质上是 React 从'保护组件实例'转向'拥抱函数式纯净'的最后一步

过去,React 像一个严厉的家长,担心我们将 ref 乱传导致指向混乱,所以设置了 forwardRef 这个门槛。

如今,在函数组件和编译器的双重护航下,React 终于相信开发者可以处理好 ref 与 props 的关系。这个'门槛'的拆除,让 React 的代码看起来更像是一段自然的、毫无框架痕迹的 JavaScript 代码。

四、use Hook

React 19 引入的 use 实际上是 Hooks 的 "增强版" 。它最大的突破就是:它可以在条件语句和循环中运行。

注:use并不是一个hook,只是一个API

1.react18的context问题

在react 18中有一个比较大的弊端,那就是hook只能在组件的顶层去定义,而且不可以放在条件判断或者循环中去使用。

假设你有一个权限系统,只有管理员(Admin)才需要读取一个巨大的 PermissionsContext。

javascript 复制代码
function Dashboard({ isAdmin }) {
  // 即使 isAdmin 为 false,你也必须在顶层调用它
  // 这意味着每个普通用户都在订阅这个庞大的上下文
  const permissions = useContext(PermissionsContext); 

  if (!isAdmin) return <p>普通用户界面</p>;

  return <AdminPanel data={permissions} />;
}

性能开销:在 React 中,只要你 useContext,那么当 Context 的值变化时,该组件一定会重新渲染。这意味着成千上万的普通用户,会因为一个他们根本用不到的"管理员权限数据"更新而被迫重新渲染组件。

use 打破了"顶层限制"。你可以把订阅逻辑藏在逻辑判断里:

jsx 复制代码
function Dashboard({ isAdmin }) {
  if (isAdmin) {
    // 只有当程序运行到这里时,React 才会建立组件与 Context 的订阅关系
    const permissions = use(PermissionsContext); 
    return <AdminPanel data={permissions} />;
  }
  return <p>普通用户界面</p>;
}

优化结果:对于非管理员,React 根本不会把这个组件挂载到 PermissionsContext 的监听队列里。这实现了真正的"逻辑按需加载"。

2.直接处理异步 ------ 渲染逻辑的"同步化"

React 团队希望开发者读取数据(无论是 Context 还是 Promise)时,能像读取普通变量一样自然,而不是非要包一层 useEffect。

这是 React 19 最具革命性的变化。它让"异步取数据"这件事,在代码观感上变成了"同步取变量"。

1. 传统模式:状态的"中间商"

在以前,数据流是断裂的:

  1. 渲染组件(显示 Loading)。

  2. useEffect 启动(异步拿数据)。

  3. 数据回来,setData(触发二次渲染)。

  4. 再次渲染组件(显示数据)。 开发者必须手动维护这个"中间状态"。

2. React 19 use(Promise) 模式:数据直达

use 让 React 组件具备了"等待"的能力。它不再需要中间状态变量,而是直接深度集成到 React 的 Suspense(悬停机制) 中。

jsx 复制代码
function Message({ messagePromise }) {
  // 1. 如果 Promise 还在 pending,React 暂停当前组件执行,直接跳出
  // 2. 此时,外层的 <Suspense> 捕获并显示 fallback(比如加载动画)
  // 3. 当 Promise 完成(Resolved),React 自动回到这里,把结果给到 content
  const content = use(messagePromise); 
  
  return <p>{content}</p>;
}

我们可以举一个例子来看

(1) React 18 的做法:繁琐的"副作用手动挡"

在 React 19 之前,异步获取数据是一场关于 useEffect、useState 和"时机管理"的博弈。

scss 复制代码
// React 18 典型写法
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 每次 userId 变了,都要手动重置状态
    setLoading(true);
    setError(null);

    fetchUser(userId)
      .then((data) => {
        setUser(data); // 手动存入状态
      })
      .catch((err) => {
        setError(err); // 手动处理错误
      })
      .finally(() => {
        setLoading(false); // 手动关闭加载
      });
  }, [userId]); // 必须死盯着依赖数组

  if (loading) return <div>加载中...</div>;
  if (error) return <div>出错了:{error.message}</div>;
  if (!user) return null;

  return <h1>{user.name}</h1>;
}

痛点总结:

  1. 状态冗余:为了显示一个数据,你额外创建了 3 个状态(user, loading, error)。

  2. 流程支离破碎:数据获取逻辑被迫写在 useEffect 里,与 UI 渲染逻辑完全分离。

  3. "瀑布流"困境:这种写法通常会导致组件先渲染出一个空的"壳子",然后再去加载数据,体验不够流畅。

(2) React 19 的做法:直觉的"渲染即同步"

在 React 19 中,你可以直接在渲染过程中"读取"异步结果。代码看起来就像是同步执行的一样。

javascript 复制代码
// React 19 写法
import { use } from 'react';

function UserProfile({ userPromise }) {
  // 直接"解开"这个 Promise,就像读取一个普通变量
  const user = use(userPromise); 

  // 代码执行到这里时,user 已经是请求成功后的真实数据了
  return <h1>{user.name}</h1>;
}

// 在父组件中使用
function App() {
  const userPromise = fetchUser(123); // 启动异步请求

  return (
    // 错误处理交给外层的 ErrorBoundary
    <ErrorBoundary fallback={<div>出错了</div>}>
      {/* 加载状态交给外层的 Suspense */}
      <Suspense fallback={<div>加载中...</div>}>
        <UserProfile userPromise={userPromise} />
      </Suspense>
    </ErrorBoundary>
  );
}

3. 为什么这个极其重要?

消除竞态条件: 以前你在 useEffect 里发请求,如果请求还没回来 id 就变了,你需要写逻辑去忽略旧请求。而 use 是在渲染流程里的,如果 id 变了,React 会直接处理新的 Promise,旧的会自动被废弃。

组件更纯粹: 组件变成了一个真正的"视图生成器"。它不再是一个管理 Fetch 逻辑的状态机,而是一个 "给它 Promise,它就吐出 UI" 的纯函数。

结语

回顾 React 19 的这些重磅更新,你会发现它们并不是在盲目追求新功能,而是共同在做一个减法------消除"框架带来的心智负担"。

在过去很长一段时间里,React 开发者其实背负着沉重的"框架税":为了性能,我们得手动管理 useMemo 的依赖数组;为了处理异步,我们得在 useEffect 里编写重复的加载与错误逻辑;为了穿透 ref,我们得忍受 forwardRef 那样拗口的语法。这些逻辑虽然必要,但它们与业务本身无关,更像是为了"迁就" React 的局限性而写的方言。

React 19 的核心命题,就是通过底层的自动化(Compiler)与原生化(Actions / use API),让开发者从一个"React 调优师"重新变回一个"JavaScript 工程师"。 它解决的不仅是代码行数的问题,更是前端开发的第一性原理问题:如果框架足够聪明,开发者就应该只关注 UI 如何响应数据,而不必去操心异步任务的生命周期、复杂的引用缓存或是繁琐的转发逻辑。

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax