brickspert/blog

如何升级到 React 18

brickspert opened this issue · 1 comments

这是 React 官方 2022.03.08 发表的文章《How to Upgrade to the React 18 Release Candidate》的译文,通过本文,可以对 React 18 的新特性有一个全面的认知。

接下来,我还会翻译其它几篇比较重要的 React 18 文章,以便以更好的姿势使用 React 18,关注不迷路。

今天,我们发布了 React 18 RC 版本。正如我们在 React Conf 上分享的那样,React 18 基于 concurrent 模式,带来了更多能力,同时提供了渐进升级的方法。在这篇文章中,我们会一步一步的带您升级到 React 18。

安装

使用 @rc标签来安装最新版 React

## npm
$ npm install react@rc react-dom@rc

## yarn
$ yarn add react@rc react-dom@rc

客户端渲染 API 更新

当你首次安装 React 18 的时候,你会看到如下警告

ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it’s running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

React 18 提供了更合理的初始化 API,使用该 API,会自动启用 concurrent 模式:

// 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);
root.render(<App tab="home" />);

同时我们将卸载方法从 unmountComponentAtNode 修改为 root.unmount

// Before
unmountComponentAtNode(container);

// After
root.unmount();

我们移除了 ReactDOM.render 函数的 callback,因为当使用 Suspense 的时候,它会有问题:

// Before
const container = document.getElementById('app');
ReactDOM.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 = ReactDOM.createRoot(container);
root.render(<AppWithCallbackAfterRender />);

最后,如果你使用 hydration 来实现了 SSR,需要将 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.

更多信息可见 Replacing render with createRoot

SSR API 更新

在 React 18 中,为了支持服务端的 Suspense 和流式 SSR,优化了 react-dom/server 的 API。

使用以下 API,将会抛出警告:

  • renderToNodeStream:废弃 ⛔️️

相反,对于 Node 环境中的流式传输,请使用:

  • renderToPipeableStream:新增

我们还引入了一个新的 API,以在现代边缘运行时环境支持流式 SSR 和 Suspense,例如 Deno 和 Cloudflare workers:

  • renderToReadableStream:新增

下面的两个 API 可以继续使用,但是不支持 Suspense:

  • renderToString:限制 ⚠️
  • renderToStaticMarkup:限制 ⚠️

下面的 API 没有变化:

  • renderToStaticNodeStream

更多信息可见Upgrading to React 18 on the serverNew Suspense SSR Architecture in React 18

自动批处理 Automatic Batching

批处理是指:React 将多个状态更新,聚合到一次 render 中执行,以提升性能。

在 React 18 之前,只能在 React 自己的事件机制中使用批处理,而在 Promise、setTimeout、原生事件等场景下,是不能使用批处理的。

React 18 支持了更多场景下的批处理,以提供更好的性能。

// 在 React 18 之前,只有 React 事件,才会使用批处理

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 会 render 两次,每次 state 变化更新一次
}, 1000);

使用 createRoot初始化 React 18 之后,所有的状态更新,会自动使用批处理,不关心应用场景。

// React 18 之后,Promise、setTimeout、原生事件中,都会自动批处理

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 只会 re-render 一次,这就是批处理
}, 1000);

这是一个 break change,但是我们希望这能提升你的产品性能。当然,你仍然可以使用 flushSync 来手动取消批处理,强制同步执行:

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React 更新一次 DOM
  flushSync(() => {
    setFlag(f => !f);
  });
  // React 更新一次 DOM
}

更多信息可见 Automatic batching for fewer renders in React 18

三方库 API

在 React 18 中,我们和三方库作者合作,定义了一些新的 API,以满足三方库在 concurrent 模式下特定场景的诉求。比如 styles 管理、外部状态管理、可访问性(accessibility)等场景。

为了支持 React 18,一些三方库可能需要用到下面的 API:

  • useId 是一个新的 Hook,支持在客户端和服务端生成唯一的 ID,同时避免 hydration 的不兼容。它可以解决在 React 17 及更低版本一直存在的问题。在 React 18 中,这个问题尤为重要,因为流式 SSR 返回的 HTML 片段是无序的。更多信息可见 Intent to Ship: useId

  • useSyncExternalStore是一个新的 Hook,允许外部状态管理器,强制立即同步更新,以支持并发读取。这个新的 API 推荐用于所有 React 外部状态管理库。详情见 useSyncExternalStore overview postuseSyncExternalStore API details

  • useInsertionEffect是一个新的 Hook,它可以解决 CSS-in-JS 库在渲染中动态注入样式的性能问题。除非你已经构建了一个 CSS-in-JS 库,否则我们不希望你使用它。这个 Hook 执行时机在 DOM 生成之后,Layout Effect 执行之前。更多信息可见 Library Upgrade Guide for style

React 18还为 concurrent 渲染引入了新的 API,例如 startTransitionuseDeferredValue,在即将发布的稳定版本中会分享更多相关内容。

严格模式 Strict Mode

未来,我们希望添加一个功能,允许 React 保存组件的状态,但移除 UI 部分。比如在返回旧的页面时,React 立即恢复之前的内容。为此,React 将使用之前保留的状态重新加载组件。

这个功能会给 React 项目带来非常好的体验,但要求组件支持 state 不变的情况下,组件多次卸载和重载。

为了检查出不合适的组件写法,React 18 在开发模式渲染组件时,会自动执行一次卸载,再重新加载的行为,以便检查组件是否支持 state 不变,组件卸载重载的场景。

在以前,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

更多信息可见: Adding Strict Effects to Strict ModeHow to Support Strict Effects

配置测试环境

当你第一次在测试用例中使用 createRoot时候,你会看到以下警告:

The current testing environment is not configured to support act(…)

为了修复这个问题,你需要在执行用例之前设置 globalThis.IS_REACT_ACT_ENVIRONMENTtrue

// In your test setup file
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

这个标记告诉 React,它在一个类似单元测试的环境中运行。如果你忘了使用 act,React 将打印一些有用的警告。
你也可以将标志设置为 false 来告诉 React 不需要 act。 这对于模拟浏览器环境的端到端测试很有用。
当然,我们希望测试库会自动为您加上这个配置。 例如,下一个版本的 React Testing Library 内置了对 React 18 的支持,无需任何额外配置。

更多信息可见:More background on the the act testing API and related changes

移除了 IE 支持

在此版本中,React 将放弃对 Internet Explorer 的支持。我们进行此更改是因为 React 18 中引入的新功能是基于现代浏览器开发的,部分能力在 IE 上是不支持的,比如 microtasks。

如果您需要支持 Internet Explorer,我们建议您继续使用 React 17。

其它变更

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

大佬,想到一个问题:useId 能替代 uuid 吗?