主题
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>开启严格模式后,组件将会:
- 重新渲染一次,以查找由于非纯渲染而引起的错误
- 重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误
- 检查是否使用了已弃用的 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");
// ...
}