Skip to content

React 19 使用笔记 ​

汇总 React 19 的变动(ref 作为普通 prop、ref 清理函数、<Context> 直接作为提供者、文档元数据、样式表与异步脚本等)与新增 Hook(useOptimistic、use、useActionState、useFormStatus),以及 React 18 以来常用的组件和 API 用法。

一、React 19 的变动 ​

1. ref 作为普通的 prop 传递 ​

不再需要使用 forwardRef,直接从 props 中读取 ref:

tsx
function MyInput({ placeholder, ref }) {
  return <input placeholder={placeholder} ref={ref} />;
}

// <MyInput ref={ref} />

2. refs 支持清理函数 ​

在 ref 改变时取消订阅事件。ref 回调可以返回一个清理函数,当元素从 DOM 中被移除时调用它来重置 ref:

tsx
<input
  ref={(ref) => {
    // ref 创建

    // 新特性:当元素从 DOM 中被移除时
    // 返回一个清理函数来重置 ref
    return () => {
      // ref cleanup
    };
  }}
/>

提示:支持清理函数后,ref 回调不能再隐式返回其他值,需要写成带花括号的函数体,否则会因返回值类型不符报错。

3. <Context.Provider> 简化为 <Context> ​

在 React 19 之前,Context 上下文需要使用 <Context.Provider> 来作为上下文提供者。在 React 19 之后,可以直接将 <Context> 渲染为提供者,无需再使用 <Context.Provider>:

tsx
// React 19 之前
<MyContext.Provider value={...}>
  <Page />
</MyContext.Provider>

// React 19
<MyContext value={...}>
  <Page />
</MyContext>

4. useDeferredValue 支持初始值 ​

useDeferredValue 添加了一个 initialValue 选项:

tsx
function Search({ deferredValue }) {
  // 初始渲染时 value 为 ''
  // 随后会安排一次使用 deferredValue 的重新渲染
  const value = useDeferredValue(deferredValue, "");

  return <Results query={value} />;
}

当提供了 initialValue,useDeferredValue 将在组件的初始渲染中返回它作为 value,并在后台安排一个使用返回的 deferredValue 重新渲染。

5. 原生支持文档元数据 ​

React 19 原生支持 <title>、<meta> 和 <link> 等文档元数据标签。这些标签可直接在组件中声明,React 会自动将它们提升至 <head>:

tsx
function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <title>{post.title}</title>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
      <p>...</p>
    </article>
  );
}

6. 支持样式表 ​

如果你告诉 React 样式表的 precedence,它将管理样式表在 DOM 中的插入顺序,并确保在显示依赖于这些样式规则的内容之前加载样式表(如果是外部的):

tsx
function ComponentOne() {
  return (
    <Suspense fallback="loading...">
      <link rel="stylesheet" href="foo" precedence="default" />
      <link rel="stylesheet" href="bar" precedence="high" />
      <article className="foo-class bar-class">{/* ... */}</article>
    </Suspense>
  );
}

7. 支持异步脚本 ​

可以在组件内部注册异步脚本:

tsx
function MyComponent() {
  return (
    <div>
      <script async={true} src="..." />
      Hello World
    </div>
  );
}

8. DNS 预解析与预连接 ​

react-dom 提供 prefetchDNS 与 preconnect,会在渲染时把预加载 link 标签安全地插入到文档的 <head> 中。具体用法见后文「六、react-dom API」中的 preconnect 与 prefetchDNS 两节。

二、新增 Hook ​

1. useOptimistic ​

乐观更新:允许先设定并显示接口返回的预期值,如果请求失败则恢复更新前的值。

2. use ​

use 具有两个主要功能:

  • 与 useContext 类似,可以直接读取上下文 Context 中提供的 value 值(可以在 if 语句中读取,因为它不是 Hook,而是 React API)
  • 可以读取类似于 Promise 的值,并等待其解析(需与 Suspense 结合使用)

1. 读 Context ​

tsx
import { useState, useCallback, createContext, use } from "react";

const defaultValue = { count: 0, handleClick: () => {} };

// 1. 创建 Context
const MyContext = createContext(defaultValue);

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

  const handleClick = useCallback(() => {
    setCount((prev) => prev + 1);
  }, []);

  // 2. 提供 Context 值
  return (
    <MyContext value={{ count, handleClick }}>
      <MyButton />
    </MyContext>
  );
};

const MyButton = () => {
  // 3. 消费 Context
  const { count, handleClick } = use(MyContext);

  return (
    <div>
      {count}
      <button onClick={handleClick}>按钮</button>
    </div>
  );
};

export default App;

2. 读类 Promise,需结合 Suspense 组件使用 ​

tsx
import { useState, use, Suspense } from "react";

const getData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([Math.random(), Math.random()]);
    }, 1000);
  });
};

const App = () => {
  const [newsPromise, setNewsPromise] = useState(getData);

  const handleUpdate = () => {
    setNewsPromise(getData);
  };

  return (
    <>
      <h3>
        数据列表 <button onClick={handleUpdate}>刷新</button>
      </h3>
      <Suspense fallback={<p>获取数据中...</p>}>
        <News newsPromise={newsPromise} />
      </Suspense>
    </>
  );
};

const News = ({ newsPromise }) => {
  const data = use(newsPromise);

  return (
    <ul>
      {data.map((title, index) => (
        <li key={index}>{title}</li>
      ))}
    </ul>
  );
};

export default App;

3. useActionState ​

常用于服务端渲染中处理表单提交的异步操作:

tsx
// 使用表单的 Actions 和 useActionState
function ChangeName({ name, setName }) {
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null,
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </form>
  );
}

4. useFormStatus ​

读取父 <form> 的状态,就像表单是一个 Context 提供者一样:

tsx
import { useFormStatus } from "react-dom";

function DesignButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending} />;
}

提示:useFormStatus 只能在 <form> 的子组件中调用,父组件里读取不到 pending 状态。

三、React 组件 ​

1. Fragment ​

使用 Fragment 可以让我们不需要在 DOM 中添加额外的节点,只需要用 React.Fragment 或者简写 <>...</> 来包裹内容就行了。

简写 <>...</> 与 Fragment 的区别是:Fragment 可以支持 key 属性,简写不支持 key 属性。

2. Profiler ​

<Profiler> 允许你编程式测量 React 树的渲染性能:

tsx
<Profiler id="App" onRender={onRender}>
  <App />
</Profiler>
  • id:唯一 id 值
  • onRender:回调函数,当包裹的组件树更新时 React 都会调用它,它接收有关渲染内容和所花费时间的信息
tsx
function onRender(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
) {
  // 对渲染时间进行汇总或记录...
}

<Profiler> 可以包裹在任意组件上,只需提供唯一 id 用以区分。可以使用该组件测试页面的性能,例如收集渲染树时长大于 150ms 的页面:

tsx
function reactPerfMonitor(
  id: string,
  phase: string,
  actualDuration: number,
  baseDuration: number,
  commitTime: number,
) {
  if (actualDuration <= 150) return;

  const list = [];

  list.push({
    key: nanoid(10),
    id,
    phase,
    actualDuration,
    baseDuration,
    commitTime,
    timestamp: Date.now(),
  });
}

3. StrictMode ​

开启严格模式:

tsx
<StrictMode>
  <App />
</StrictMode>

开启严格模式后,组件将会:

  1. 重新渲染一次,以查找由于非纯渲染而引起的错误
  2. 重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误
  3. 检查是否使用了已弃用的 API

4. Suspense ​

在子组件完成加载前展示后备方案,常与 lazy 懒加载配合使用:

tsx
import React, { Suspense } from "react";

const OtherComponent = React.lazy(() => import("./OtherComponent"));
const AnotherComponent = React.lazy(() => import("./AnotherComponent"));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </div>
  );
}

如果有嵌套路由,则需再使用 Suspense 组件包裹。否则初次动态加载路由、加载 JS 文件时,延迟会导致闪频。

5. Activity ​

通过 CSS 的 display: none 隐藏,但保留组件状态,同时卸载副作用:

  • 如果 props 更新,即使组件处于隐藏状态,内容也会更新
  • 即使没有挂载,也会进行一次预渲染
tsx
import { useEffect, useState, Activity } from "react";

function TabPanel({ children, isActive }) {
  return <Activity mode={isActive ? "visible" : "hidden"}>{children}</Activity>;
}

function ProfileForm({ count }) {
  return <div>ProfileForm:{count}</div>;
}

function SettingsList({ count }) {
  useEffect(() => {
    return () => {
      console.log("清除副作用");
    };
  }, []);

  return (
    <div>
      SettingsList<div>233:{count}</div>
    </div>
  );
}

function App() {
  const [activeTab, setActiveTab] = useState("profile");
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setActiveTab("profile")}>个人资料</button>
      <button onClick={() => setActiveTab("settings")}>设置</button>
      <button onClick={() => setCount(count + 1)}>点击次数:{count}</button>

      {/* 个人资料面板:含表单输入 */}
      <TabPanel isActive={activeTab === "profile"}>
        <ProfileForm count={count} /> {/* 假设包含姓名、邮箱等输入框 */}
      </TabPanel>

      {/* 设置面板:含开关、下拉框等控件 */}
      <TabPanel isActive={activeTab === "settings"}>
        <SettingsList count={count} /> {/* 假设包含通知设置、隐私选项等 */}
      </TabPanel>
    </div>
  );
}

export default App;

四、React API ​

1. createContext ​

创建组件能够提供与读取的上下文:

tsx
const MyContext = createContext(defaultValue);

用上下文的 Provider 包裹组件,为所有被包裹的子组件指定一个上下文的值。React 19 起可以直接用 <Context> 作为提供者、省略 .Provider,写法见前文「一、3」。

2. lazy ​

在组件第一次被渲染之前延迟加载组件的代码(常用于路由懒加载):

tsx
import { lazy } from "react";

const MarkdownPreview = lazy(() => import("./MarkdownPreview"));

通过 React.lazy 加载的组件必须使用 export default 方式导出。

3. memo ​

允许组件在 props 没有改变的情况下跳过重新渲染。默认通过 Object.is 比较每个 prop,也可以通过第二个参数传入自定义函数来控制对比过程:

tsx
function MyComponent(props) {
  /* 使用 props 渲染 */
}

function areEqual(prevProps, nextProps) {
  // 为 true 不更新,为 false 则重新渲染
  return prevProps.obj.more !== nextProps.obj.more;
}

export default React.memo(MyComponent, areEqual);

提示:如果函数组件没有 props,不需要使用 React.memo。

4. startTransition ​

可以让你在不阻塞 UI 的情况下更新 state:

tsx
startTransition(scope);

与 useTransition 的区别就是,startTransition 没有提供一种跟踪 transition 是否处于待定状态的方法:

tsx
import { startTransition } from "react";

function TabContainer() {
  const [tab, setTab] = useState("about");

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ...
}

五、react-dom 组件 ​

1. dangerouslySetInnerHTML ​

渲染 HTML 内容,例如搜索关键字高亮:

tsx
<div
  dangerouslySetInnerHTML={{
    __html: searchKeyword
      ? text.replace(searchKeyword, `<span style="color:red">$&</span>`)
      : text,
  }}
/>

六、react-dom API ​

1. createPortal ​

将 React 元素放置在 DOM 树中的任何地方。

只会影响 DOM 结构,不会影响 React 组件树;能正常接收 props 以及 context。

第一个参数是 React 元素,第二个参数 container 是一个 DOM 节点:

tsx
createPortal(child, container);

场景:对话框、悬浮框以及提示框,这些常常挂载到 body 元素上,而非组件本身。

tsx
import { createPortal } from "react-dom";

// ...

<div>
  <p>This child is placed in the parent div.</p>
  {createPortal(
    <p>This child is placed in the document body.</p>,
    document.body,
  )}
</div>;

2. flushSync ​

强制 React 刷新所有挂起的工作,并同步更新 DOM,常用于处理 React 17 升 18 所遗留的问题:

ts
flushSync(callback);

flushSync(冲掉同步)会退出批量更新:

tsx
import React, { useState } from "react";
import { flushSync } from "react-dom";

const App: React.FC = () => {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  console.log("更新");

  return (
    <div
      onClick={() => {
        // 第一次更新
        flushSync(() => {
          setCount1((count) => count + 1);
        });

        // 第二次更新
        flushSync(() => {
          setCount2((count) => count + 1);
        });
      }}
    >
      <div>count1:{count1}</div>
      <div>count2:{count2}</div>
    </div>
  );
};

export default App;

flushSync 函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。

flushSync 确保了在下一行代码运行时,React 已经更新了 DOM:

tsx
import { useRef, useState } from "react";

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

  const ref = useRef(null);

  const onClick = () => {
    setCount((count) => count + 1);

    // 打印值为 0,因为此时 DOM 还未更新
    // 如果使用 flushSync 包裹 setCount,打印结果将变为 1
    console.log("ref", ref.current.innerHTML);
  };

  return (
    <button onClick={onClick} ref={ref}>
      {count}
    </button>
  );
}

export default App;

flushSync 的作用:设置回调函数中的优先级为离散事件的优先级,即同步车道,并立即执行:

js
try {
  ReactCurrentBatchConfig.transition = null;
  setCurrentUpdatePriority(DiscreteEventPriority);
  if (fn) {
    return fn();
  } else {
    return undefined;
  }
}

并在执行完后,如果不在 render 和 commit 阶段,还会通过 flushSyncCallbacks 执行所有的同步任务,主动触发一次同步的渲染流程(render + commit),从而立即将更新反映到 DOM 上:

js
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
  flushSyncCallbacks();
}

3. preconnect ​

DNS 预连接(解析 + 连接)。React 19 起会随渲染把预加载 link 标签安全地插入到文档的 <head>:

tsx
import { preconnect } from "react-dom";

function AppRoot() {
  preconnect("https://example.com");
  // ...
}

4. prefetchDNS ​

DNS 预解析。同样会在渲染时安全地插入到文档的 <head>:

tsx
import { prefetchDNS } from "react-dom";

function AppRoot() {
  prefetchDNS("https://example.com");
  // ...
}