如何升级到React 18

160 阅读9分钟

美国时间2022年3月29号,React团队宣布React 18已经发布到了npm,Rect 18正式发布。 React官方网站上给出了如何升级到React 18的文档,这里我们把它做了翻译整理,作为大家升级React的checklist。

安装

npm:

npm install react react-dom

yarn:

yarn add react react-dom

更新客户端渲染API

老版:

import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

React 18:

import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
// 先创建root
const root = createRoot(container); // 如果是Typescript:createRoot(container!) 
// 然后调用render
root.render(<App tab="home" />);

ReactDom.render仍然可以使用,但是它是React 17的功能,没有React 18的特性,所以如果还用这个API那就等于没有升级。同时React也会提示你使用新的API。

要想取消在container上的挂载,老版和React 18的方式分别如下: 老版:

unmountComponentAtNode(container);

React 18:

root.unmount();

render的callback被移除

因为和Suspense不兼容,所以callback直接被移除。如果实在觉得callback有用,直接在顶层组件中useEffect,也能达到目的,示例代码:

老版:

const container = document.getElementById('app');
render(<App tab="home" />, container, () => {
  console.log('rendered');
});

React 18:

function AppWithCallbackAfterRender() {
  useEffect(() => {
    console.log('rendered');
  });

  return <App tab="home" />
}

const container = document.getElementById('app');
const root = createRoot(container);
root.render(<AppWithCallbackAfterRender />);

更新服务端渲染API

将hydrate改成hydrateRoot。示例代码:

老版:

import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);

React 18:

import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// 这个改动比client端小一些

为了支持Suspense,新版添加了一些服务端渲染相关的API:

  • renderToPipeableStream:替换renderToNodeStream
  • renderToReadableStream:供Deno和Cloudflare等前沿运行环境使用
  • renderToString、renderToStaticMarkup还能使用,但是对Suspense支持都有限
  • renderToStaticNodeStream用来渲染email,可继续使用

Typescript类型定义

  • 首选需要更新 @types/react和@types/react-dom。
  • 如果用到了children属性,则必须在interface中明确的定义出来,示例代码:
    interface MyButtonProps {
        color: string;
        children?: React.ReactNode;
    }
    

自动批量处理

React通过合并多个状态更新进行批处理来提升性能,React 18则进行了更多的合并。 在18之前,promise、setTimeout、原生浏览器事件等任何其他事件是不会被合并批量处理的,只有React自己的事件会被合并批量处理,看下面的示例代码:

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React只会渲染一次,setCount和setFlag合并到一起后对UI进行更新
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React会渲染两次,每次setState都会触发对UI的更新
}, 1000);

React 18中,setTimeout、promise、浏览器原生事件等任何其他事件中的setState都会被批量处理。 示例代码:

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React只会渲染一遍
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React只会渲染一遍
}, 1000);

这个更新不是向后兼容的,大家需要注意一下。 如果你不想React批量更新,可以用flushSync让React提前将当前状态渲染出来:

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // 代码运行到这里时,React已经更新了DOM
  flushSync(() => {
    setFlag(f => !f);
  });
  // 代码运行到这里时,React再次的更新了DOM
}

其实这种批量操作无处不在,浏览器、显卡都在做类似的事情来优化性能。

供一些库使用的新API

  • useSyncExternalStore 这个hook主要是给第三方的状态管理库使用。普通应用开发者基本上不需要用它。React 18的分片执行可以缓解因大量的渲染任务带来的UI卡顿,之前的每次渲染都是一个原子性的操作,不可分割,但是现在分片之后便会有数据不一致的可能,这和数据库的原子性操作概念类似,React的状态管理功能很好的解决了这些问题,开箱即用,不用用户操心,但是如果用户没有使用React自身的状态管理,而是使用了第三方的状态管理,数据不一致的问题便会暴露出来。useSyncExternalStore就是用来解决这个问题的。具体的原理咱们放到另一篇专门讲React 18原理的专题里详细解释,敬请关注。
  • useInsertionEffect 这个hook是给CSS-in-JS使用的,用于解决性能问题。React团队并不希望你在其他地方使用它。 这个hook在DOM被修改之后且layout effect读取新的layout之前运行,这个咱们也放到另一个篇文章单独讲解。毕竟要用到这个hook的人很少。

配置测试环境

当你将测试环境更新到React 18,并且换上了createRoot之后,你会在测试控制台看到一个警告: The current testing environment is not configured to support act(…)

要解决掉这个警告,需要设置:

globalThis.IS_REACT_ACT_ENVIRONMENT = true

意思是告诉React,当前运行环境是单元测试,React会主动支持测试工具的act函数,这时如果你没有正确调用act,React还会提醒你。 这里简单说一下act,act是React测试工具库提供的一个helper,由于React的渲染是一个分步异步的过程,例如某个state初始值是0,mount完成后该state设置成1,如果我们测试代码中在render方法后立即检测渲染结果,会得到0,只有稍过一段时间,渲染结果才能变成1,而我们的断言肯定希望是1,这个测试就会失败。有了act之后,触发渲染的代码用act包裹后,里面的setState和排队的effect函数会全部在断言之前执行完,这样渲染结果是1,断言也和预期一致。

如果你不想用act,可以将上面这个开关设置成false。

React 18不再支持IE

微软已经不在维护IE,且React 18使用像微任务这样的现代浏览器特性创建,这些特性在IE上无法被充分的polyfill。 如果你需要支持IE,那建议你还是继续使用React 17。

几个被废弃的API

  • 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 18会同步的运行所有的effect函数
  • 服务端渲染时,hydration变得更严格:服务端渲染时,因为丢失或者多出额外的文本内容而导致的服务端和客户端html不一致将被当作错误对待而不仅仅是一个警告。如果客户端和服务端不一致,React 18不再尝试为了和服务端保持一致而在客户端主动的去插入或者删除节点,而是直接显示最近的suspense fallback的内容。这主要是为了确保不会因此带来潜在的隐私和安全问题
  • Suspense树总是一致的:如果一个组件在添加到节点树之前是sunpense状态,React 18则不会在state还不完整的情况下把组件添加进节点树或者执行它的effect函数。相反,React 18会等异步操作都完成,然后尝试用concurrent的方式从头渲染,浏览器不会卡住
  • Suspense下的Layout Effects:当节点数再次进入suspense并且用fallback显示时,React 18将清除layout effect,然后在boundary内的内容再次显示时重新创建。这个解决了之前组件库在用Suspense时不能正确的拿到layout的问题。
  • 需要新的Javascript环境:React 18依赖新的Javascript特性,包括:Promise、Symbol、Object.assign。所以对老版浏览器支持有限,这个大家需要注意一下,提前加好polyfill

其他显著的改变

React

  • 组件现在可以渲染undefined:如果你在组件中直接返回undefined,React 18不再给出错误信息。 主要有以下几个原因:

    • 其实对于React本身来说undefined并不会导致React不能工作,React只是怕用户忘记进行有效的return,另外React并没有办法知道用户是主动return的undefined还是疏忽了,所以怎么看这个事情都应该由lint工具去做,lint可以很好的区分用户是否真的忘记进行有效的return了
    • 我们需要到处检查,防止返回了undeinfed
    • 对于类型系统,不能创建一个通用的组件类型。当组件直接返回它的children时,即使类型是匹配的,仍然需要去判断组件的children是否是undefined。所以放开undefined也是为了让类型系统自洽。
    • 最新的Suspense是允许null或者undefined的fallback的,如果组件还不能返回undefined的话,就导致了不一致
  • 自动化测试中,act警告可选择关掉了:如果你在进行end-to-end的test,act是不需要的,因此警告也就更不需要了,可以通过设置:globalThis.IS_REACT_ACT_ENVIRONMENT = false来关掉act

  • 对于卸载或者未挂载的组件进行setState不再发出警告信息 警告中的内容是说会造成内存泄漏。这里指的内存泄漏实际上只有一种情况下会发生,就是当我们在用store.subscribe时,如果给一个unmount的组件在subscribe了setState的函数,则有可能没有机会unsubscribe,因为unsubscribe一般都是在unmout事件中进行的,从而导致内存泄漏。在实际开发中,我们直接使用store.subscribe的情况太少见了,为了去除这个警告而加很多无谓的判断,让代码变得复杂,确实没有太大必要

  • console.log不在被掐掉 在Strict模式下,React会渲染每个组件两遍来帮你找出组件的副作用。在React 17中,为了更方便看log,两遍渲染中其中一遍的console.log会被掐掉。社区的反馈是这个操作很容易让人迷惑,为了响应社区的要求,React 18不再掐掉console.log,两遍都会显示出来,但是我们在React DevTools中将第二遍log以灰色显示

  • 内存占用得到了优化 React 18在unmount中清除了很多内部的字段,这会让你应用中那些没有被修复的内存泄漏问题变得没那么严重

React DOM Server

  • renderToString 当服务端处于suspense状态时,renderToString不再报错了,而是直接显示最近Suspense组件的fallback。还是建议升级到renderToPipeableStream或者renderToReadableStream
  • renderToStaticMarkup 当服务端处于suspense状态时,renderToString不再报错了,而是直接显示最近Suspense组件的fallback。

React Native

因为React 18依赖新的React Native架构,React Native在未来的新版本中将会和React 18一起发布。

就是这些了。