阅读视图

发现新文章,点击刷新页面。

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

前言

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

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

告别useMemo、useCallback、React.memo

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

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

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

React18的写法

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的写法

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 写法

const count = ref(0);

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

React 18 写法

const [count, setCount] = useState(0);

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

React 19 写法

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

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中的写法是这样的

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的乐观更新

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的乐观更新

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 的痛苦写法

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

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

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。

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

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

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

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

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

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(悬停机制) 中。

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 和“时机管理”的博弈。

// 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 中,你可以直接在渲染过程中“读取”异步结果。代码看起来就像是同步执行的一样。

// 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 如何响应数据,而不必去操心异步任务的生命周期、复杂的引用缓存或是繁琐的转发逻辑。

❌