如何升级到 React 18
2022年3月8日 作者:Rick Hanlon
正如我们在发布文章中分享的那样,React 18 引入了由我们新的并发渲染器提供支持的功能,并为现有应用程序采用了一种渐进式策略。在这篇文章中,我们将指导您完成升级到 React 18 的步骤。
请报告您在升级到 React 18 过程中遇到的任何问题。
安装
要安装最新版本的 React
npm install react react-dom
或者如果您使用的是 yarn
yarn add react react-dom
客户端渲染 API 的更新
第一次安装 React 18 时,您会在控制台中看到警告
React 18 引入了一个新的根 API,它为管理根提供了更好的人体工程学。新的根 API 还启用了新的并发渲染器,允许您选择加入并发功能。
// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);
// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(<App tab="home" />);
我们还将unmountComponentAtNode
更改为root.unmount
// Before
unmountComponentAtNode(container);
// After
root.unmount();
我们还从 render 中删除了回调,因为在使用 Suspense 时,它通常不会产生预期的结果。
// Before
const container = document.getElementById('app');
render(<App tab="home" />, container, () => {
console.log('rendered');
});
// After
function AppWithCallbackAfterRender() {
useEffect(() => {
console.log('rendered');
});
return <App tab="home" />
}
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<AppWithCallbackAfterRender />);
最后,如果您的应用程序使用服务器端渲染和 hydration,请将hydrate
升级到hydrateRoot
// Before
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);
// After
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.
更多信息,请参阅此处的工作组讨论。
服务器渲染 API 的更新
在这个版本中,我们正在改进我们的react-dom/server
API,以完全支持服务器上的 Suspense 和流式 SSR。作为这些更改的一部分,我们已弃用旧的 Node 流式 API,该 API 不支持服务器上的增量 Suspense 流式传输。
使用此 API 现在会发出警告
renderToNodeStream
:已弃用 ⛔️️
相反,对于 Node 环境中的流式传输,请使用
renderToPipeableStream
:新增 ✨
我们还引入了一个新的 API,以支持使用 Suspense 为现代边缘运行时环境(例如 Deno 和 Cloudflare Workers)进行流式 SSR。
renderToReadableStream
:新增 ✨
以下 API 将继续工作,但对 Suspense 的支持有限
renderToString
:有限 ⚠️renderToStaticMarkup
:有限 ⚠️
最后,此 API 将继续用于渲染电子邮件
renderToStaticNodeStream
有关服务器渲染 API 更改的更多信息,请参阅有关在服务器上升级到 React 18的工作组文章,深入了解新的 Suspense SSR 架构,以及Shaundai Person 的关于使用 Suspense 进行流式服务器渲染在 React Conf 2021 上的演讲。
TypeScript 定义的更新
如果你的项目使用 TypeScript,你需要将你的 @types/react
和 @types/react-dom
依赖项更新到最新版本。新的类型更安全,可以捕获以前类型检查器忽略的问题。最显著的变化是 children
属性现在在定义属性时需要显式列出,例如
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}
查看 React 18 类型声明的 pull request 以获取完整的仅类型更改列表。它链接到库类型中的示例修复,因此您可以了解如何调整代码。您可以使用 自动迁移脚本 来帮助更快地将应用程序代码移植到新的更安全的类型声明。
如果您在类型声明中发现错误,请在 DefinitelyTyped 仓库中 提交问题。
自动批处理
React 18 通过默认执行更多批处理来改进性能。批处理是指 React 将多个状态更新分组到单个重新渲染中以提高性能。在 React 18 之前,我们只在 React 事件处理程序内进行批处理更新。在 Promise、setTimeout、原生事件处理程序或任何其他事件中的更新默认情况下不会在 React 中进行批处理。
// Before React 18 only React events were batched
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will render twice, once for each state update (no batching)
}, 1000);
从 React 18 开始使用 createRoot
,所有更新都将自动进行批处理,无论它们源自何处。这意味着在超时、Promise、原生事件处理程序或任何其他事件中的更新将与 React 事件中的更新一样进行批处理。
// After React 18 updates inside of timeouts, promises,
// native event handlers or any other event are batched.
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}, 1000);
这是一个重大更改,但我们预计这将减少渲染工作量,从而提高应用程序的性能。要选择退出自动批处理,可以使用 flushSync
。
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now
}
更多信息,请参阅 自动批处理深度探讨。
库的新 API
在 React 18 工作组中,我们与库维护者合作,创建了支持并发渲染的新 API,用于特定于他们在样式和外部存储等领域用例的用例。为了支持 React 18,一些库可能需要切换到以下 API 之一。
useSyncExternalStore
是一个新的 Hook,允许外部存储通过强制同步更新存储来支持并发读取。对于任何与 React 外部状态集成的库,都推荐使用此新 API。更多信息,请参阅 useSyncExternalStore 概述文章 和 useSyncExternalStore API 详情。useInsertionEffect
是一个新的 Hook,允许 CSS-in-JS 库解决在渲染中注入样式的性能问题。除非您已经构建了 CSS-in-JS 库,否则我们不希望您使用它。此 Hook 将在 DOM 被修改后但布局效果读取新布局之前运行。这解决了 React 17 及以下版本中已存在的问题,但在 React 18 中更为重要,因为 React 在并发渲染期间会让位于浏览器,从而使其有机会重新计算布局。更多信息,请参阅 库升级指南,关于<style>
。
React 18 还引入了用于并发渲染的新 API,例如 startTransition
、useDeferredValue
和 useId
,我们在 发布文章 中对此进行了更多介绍。
严格模式的更新
将来,我们希望添加一项功能,允许 React 添加和删除 UI 部分,同时保留状态。例如,当用户从屏幕切换到另一个屏幕再返回时,React 应该能够立即显示之前的屏幕。为此,React 将使用与之前相同的组件状态卸载和重新安装树。
此功能将使 React 具有更好的开箱即用性能,但需要组件能够承受多次安装和销毁效果。大多数效果无需任何更改即可工作,但某些效果假设它们只安装或销毁一次。
为了帮助发现这些问题,React 18 在严格模式中引入了一项新的仅开发检查。此新检查将在组件首次安装时自动卸载和重新安装每个组件,并在第二次安装时恢复先前状态。
在此更改之前,React 将安装组件并创建效果。
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
在 React 18 的严格模式下,React 将在开发模式下模拟卸载和重新安装组件。
* React mounts the component.
* Layout effects are created.
* Effect effects are created.
* React simulates unmounting the component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates mounting the component with the previous state.
* Layout effect setup code runs
* Effect setup code runs
更多信息,请参阅工作组文章 向严格模式添加可重用状态 和 如何在效果中支持可重用状态。
配置你的测试环境
当您首次更新测试以使用 createRoot
时,您可能会在测试控制台中看到此警告。
要解决此问题,请在运行测试之前将 globalThis.IS_REACT_ACT_ENVIRONMENT
设置为 true
。
// In your test setup file
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
此标志的目的是告诉 React 它正在单元测试之类的环境中运行。如果您忘记使用`act
` 包装更新,React 将记录有用的警告。
您也可以将标志设置为`false
`,以告诉 React 不需要`act
`。这对于模拟完整浏览器环境的端到端测试非常有用。
最终,我们希望测试库会自动为您配置此项。例如,下一版本的 React Testing Library 内置支持 React 18,无需任何额外配置。
有关`act
` 测试 API 和相关更改的更多背景信息可在工作组中找到。
放弃对 Internet Explorer 的支持
在此版本中,React 停止支持 Internet Explorer,将于 2022 年 6 月 15 日停止支持。我们现在进行此更改是因为 React 18 中引入的新功能是使用现代浏览器功能(例如微任务)构建的,这些功能无法在 IE 中充分进行 polyfill。
如果您需要支持 Internet Explorer,我们建议您继续使用 React 17。
弃用功能
react-dom
: `ReactDOM.render
` 已弃用。使用它将发出警告并在 React 17 模式下运行您的应用程序。react-dom
: `ReactDOM.hydrate
` 已弃用。使用它将发出警告并在 React 17 模式下运行您的应用程序。react-dom
: `ReactDOM.unmountComponentAtNode
` 已弃用。react-dom
: `ReactDOM.renderSubtreeIntoContainer
` 已弃用。react-dom/server
: `ReactDOMServer.renderToNodeStream
` 已弃用。
其他重大更改
- 一致的 useEffect 定时:如果更新是在离散的用户输入事件(例如点击或按键事件)期间触发的,React 现在始终同步刷新 effect 函数。以前,行为并不总是可预测或一致的。
- 更严格的水合错误:由于缺少或多余的文本内容导致的水合不匹配现在被视为错误而不是警告。React 将不再尝试通过在客户端插入或删除节点来“修补”单个节点以匹配服务器标记,而是将恢复到客户端渲染,直到树中最接近的`
<Suspense>
` 边界。这确保了水合树的一致性,并避免了水合不匹配可能造成的潜在隐私和安全漏洞。 - Suspense 树始终保持一致:如果组件在完全添加到树之前暂停,React 将不会以不完整状态将其添加到树中或触发其 effect。相反,React 将完全丢弃新树,等待异步操作完成,然后从头开始重试渲染。React 将并发渲染重试尝试,并且不会阻塞浏览器。
- 带有 Suspense 的布局 Effect:当树重新暂停并恢复到回退时,React 现在将清理布局 effect,然后在边界内的内容再次显示时重新创建它们。这修复了一个问题,该问题阻止了组件库在与 Suspense 一起使用时正确测量布局。
- 新的 JS 环境要求:React 现在依赖于现代浏览器功能,包括`
Promise
`、`Symbol
` 和`Object.assign
`。如果您支持较旧的浏览器和设备(例如 Internet Explorer),这些设备未原生提供现代浏览器功能或具有不兼容的实现,请考虑在捆绑的应用程序中包含全局 polyfill。
其他值得注意的更改
React
- 组件现在可以渲染`
undefined
`:如果您从组件中返回`undefined
`,React 将不再发出警告。这使得允许的组件返回值与在组件树中间允许的值一致。我们建议使用 linter 来防止诸如忘记在 JSX 之前使用`return
` 语句之类的错误。 - 测试中的
act
警告现已选择加入:如果您正在运行端到端测试,则act
警告是不必要的。我们引入了一种 选择加入 机制,以便您仅在单元测试中启用它们,在单元测试中它们是有用且有益的。 - 不再警告在已卸载组件上使用
setState
:以前,当您在已卸载组件上调用setState
时,React 会警告内存泄漏。此警告是为订阅添加的,但人们主要在设置状态可以接受且变通方法会使代码变差的情况下遇到此问题。我们已 移除 此警告。 - 不再抑制控制台日志:当您使用严格模式时,React 会渲染每个组件两次,以帮助您查找意外的副作用。在 React 17 中,我们抑制了其中一个渲染的控制台日志,以使日志更易于阅读。为了响应关于这令人困惑的 社区反馈,我们已移除抑制功能。相反,如果您安装了 React DevTools,第二个日志的渲染将以灰色显示,并且将有一个选项(默认情况下关闭)可以完全抑制它们。
- 改进的内存使用:React 现在会在卸载时清理更多内部字段,从而减少应用程序代码中可能存在的未修复内存泄漏的影响。
React DOM 服务器
renderToString
:在服务器上挂起时将不再出错。相反,它将发出最接近的<Suspense>
边界的回退 HTML,然后重试在客户端渲染相同的内容。仍然建议您切换到流式 API,例如renderToPipeableStream
或renderToReadableStream
。renderToStaticMarkup
:在服务器上挂起时将不再出错。相反,它将发出最接近的<Suspense>
边界的回退 HTML。
变更日志
您可以在这里查看 完整的变更日志。