React 17发布在即,我提前用RC版本做了一些项目升级和实验。本文总结React 17的新特性和最佳实践,包括新的JSX转换、事件系统改进、生命周期变更、并发模式准备、升级注意事项等方面的经验。这些经验是我在实际项目中踩坑踩出来的,希望能帮你顺利升级到React 17,充分利用新特性提升开发效率和应用性能。

一、React 17概述

React 17是React的一个重要版本,但和之前的版本不同,React 17没有引入太多面向开发者的新API。它的主要变化是底层的改进,为未来的并发模式(Concurrent Mode)打下基础。

React团队把React 17称为"垫脚石"版本(stepping stone release)。它的主要目标是:

  • 让React的升级更容易,支持渐进式升级
  • 改进事件系统,解决一些长期存在的问题
  • 引入新的JSX转换,让JSX不再依赖React全局变量
  • 为并发模式做准备,优化底层架构

虽然React 17没有太多新API,但它的底层改进对应用的性能和可维护性有很大影响。而且,React 17是未来并发模式的基础,升级到React 17是为未来做准备。

我在几个项目中升级到了React 17 RC版本,踩了一些坑,也积累了一些经验。下面分享给大家。

二、新的JSX转换

新的JSX转换是React 17最面向开发者的新特性,也是最实用的新特性。

1. 什么是新的JSX转换

在React 17之前,JSX会被编译成React.createElement调用。比如:

const element = <h1>Hello World</h1>;

会被编译成:

const element = React.createElement('h1', null, 'Hello World');

这意味着,在使用JSX的文件中,必须导入React,否则会报错。这也是为什么每个React组件文件开头都要写import React from 'react'

React 17引入了新的JSX转换,JSX不再被编译成React.createElement,而是被编译成从react/jsx-runtime导入的jsx函数:

import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { children: 'Hello World' });

这样,使用JSX的文件就不需要导入React了。

2. 新JSX转换的好处

新的JSX转换有几个好处:

  • 不需要导入React:使用JSX的文件不再需要import React from 'react',减少了样板代码
  • 更好的性能优化:新的jsx函数比React.createElement更轻量,编译器可以做更多优化
  • 更好的错误提示:新的转换方式让错误提示更准确
  • 为未来做准备:新的转换方式是React未来的方向,提前适应有好处

3. 如何启用新的JSX转换

要启用新的JSX转换,需要升级构建工具的配置:

  • Babel:升级@babel/preset-react到7.9.0以上,配置{"runtime": "automatic"}
  • TypeScript:升级TypeScript到4.1以上,配置"jsx": "react-jsx"
  • Create React App:升级react-scripts到4.0以上,自动支持
  • Next.js:升级Next.js到9.5以上,自动支持

Babel配置示例:

{
  "presets": [
    ["@babel/preset-react", {
      "runtime": "automatic"
    }]
  ]
}

TypeScript配置示例:

{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

4. 最佳实践

启用新的JSX转换之后,有几点需要注意:

  • 移除不必要的React导入:启用新转换后,很多文件中的import React from 'react'就不需要了。可以用ESLint的react/jsx-uses-react规则来检测和移除不必要的导入
  • 保留需要的React导入:如果文件中使用了React的其他API(比如useState、useEffect、React.memo等),还是需要导入。可以只导入需要的API,比如import { useState, useEffect } from 'react'
  • 第三方库兼容性:大部分第三方库都兼容新的JSX转换,但有些老的库可能还有问题。升级后要测试一下
  • 渐进式迁移:可以先在新文件中使用新转换,老文件保持不变。新转换和旧转换可以共存,不需要一次性全部迁移

我在项目中启用新JSX转换后,移除了几百个不必要的React导入,代码更简洁了。而且,新的转换方式确实让构建速度有一点提升,虽然不明显。

三、事件系统改进

React 17对事件系统做了重要改进,解决了一些长期存在的问题。

1. 事件委托的变化

在React 17之前,React会把所有事件都委托到document上。也就是说,不管你在哪个元素上绑定事件,最终都会在document上处理。

这种方式有一些问题:

  • 如果页面上有多个React应用,事件会在document上冲突
  • 如果在document上停止了事件冒泡,会影响React的事件处理
  • 和其他库(比如jQuery)的事件处理可能冲突

React 17改进了事件委托,不再把事件委托到document,而是委托到React应用的根容器(root container)上。也就是调用ReactDOM.render()的那个DOM元素。

这样,每个React应用的事件都在自己的根容器上处理,不会互相干扰。多个React应用可以共存,也更容易和其他库集成。

2. onScroll事件不再冒泡

在React 17之前,onScroll事件会冒泡,这经常导致问题。比如,父元素的onScroll会被子元素的滚动触发,导致不必要的事件处理。

React 17修复了这个问题,onScroll事件不再冒泡。这和浏览器的原生行为一致,也减少了意外的事件触发。

如果你的代码中依赖了onScroll的冒泡行为,升级后需要注意,可能需要调整代码。

3. onFocus和onBlur的变化

React 17中,onFocus和onBlur使用了原生的focusin和focusout事件,而不是focus和blur事件。

这带来了几个好处:

  • onFocus和onBlur现在可以正确地冒泡了(之前的实现有问题)
  • 事件对象的表现更一致
  • 和浏览器原生行为更接近

如果你的代码中使用了onFocus/onBlur的冒泡,升级后行为会更正确,但也要注意测试。

4. 事件池的移除

在React 17之前,React使用事件池(event pooling)来复用事件对象,减少垃圾回收。这意味着,事件对象在事件处理函数执行完之后就会被重置,不能异步使用。

比如,下面的代码在React 16中会有问题:

function handleClick(e) {
  setTimeout(() => {
    console.log(e.target); // e已经被重置了,target是null
  }, 100);
}

需要调用e.persist()来保留事件对象:

function handleClick(e) {
  e.persist();
  setTimeout(() => {
    console.log(e.target); // 正常
  }, 100);
}

React 17移除了事件池,事件对象不再被重置,可以异步使用。不再需要调用e.persist()了。

这是一个很实用的改进,减少了很多因为事件对象被重置而导致的bug。

5. 最佳实践

  • 测试事件相关的代码:升级后,重点测试事件相关的功能,尤其是onScroll、onFocus、onBlur的行为
  • 移除不必要的e.persist():React 17不再需要e.persist(),可以移除。但保留也不会有问题,只是多余的
  • 利用根容器事件委托:如果页面上有多个React应用,React 17的事件委托改进让它们可以更好地共存
  • 注意和第三方库的事件冲突:虽然React 17改进了事件系统,但和第三方库的事件冲突还是可能存在,要注意测试

四、生命周期和其他API变更

React 17对一些生命周期和API做了变更。

1. 废弃生命周期的警告

React 16.3就废弃了几个生命周期:componentWillMount、componentWillReceiveProps、componentWillUpdate。在React 17中,使用这些生命周期会有更强烈的警告。

这些废弃的生命周期应该替换为:

  • componentWillMount → constructor + componentDidMount
  • componentWillReceiveProps → getDerivedStateFromProps + componentDidUpdate
  • componentWillUpdate → getSnapshotBeforeUpdate + componentDidUpdate

如果你的项目中还在使用这些废弃的生命周期,建议尽快迁移。React 18可能会彻底移除这些生命周期。

2. getDerivedStateFromError的改进

React 17改进了getDerivedStateFromError错误边界(Error Boundary)的行为。在React 17之前,getDerivedStateFromError在渲染阶段调用,不应该有副作用。但很多人在里面写了副作用,导致问题。

React 17中,getDerivedStateFromError的行为更严格,应该只用来更新state,不应该有副作用。副作用应该放在componentDidCatch中。

如果你的错误边界中在getDerivedStateFromError里写了副作用,升级后需要调整。

3. forwardRef的改进

React 17改进了forwardRef的显示名称。在React DevTools中,forwardRef组件的显示名称现在更准确了。

之前,forwardRef组件在DevTools中显示为"ForwardRef",不容易区分。现在,会显示组件的实际名称,调试更方便。

4. 原生组件的属性支持

React 17增加了对一些原生DOM属性的支持:

  • 支持onAuxClickonAuxClickCapture(中键点击)
  • 支持onToggle事件(details元素的展开/收起)
  • 支持onContextMenu的捕获版本
  • 支持disablePictureInPicture属性(video元素)
  • 支持controlsList属性(video元素)

这些改进让React可以更好地支持原生DOM的新特性。

5. 最佳实践

  • 迁移废弃的生命周期:尽快把componentWillMount等废弃生命周期迁移到新的API
  • 正确使用错误边界:getDerivedStateFromError只更新state,副作用放在componentDidCatch
  • 利用新的原生属性支持:需要用到新的DOM属性时,直接用,不需要再用ref或dangerouslySetInnerHTML
  • 关注React DevTools的改进:升级React DevTools到最新版本,获得更好的调试体验

五、并发模式准备

React 17最重要的底层改进,是为并发模式(Concurrent Mode)做准备。

1. 什么是并发模式

并发模式是React的一个革命性特性,它让React可以中断渲染,优先处理更重要的更新。这样,即使在渲染大量内容的时候,用户交互也能保持流畅。

并发模式的核心概念:

  • 可中断渲染:React可以在渲染过程中暂停,处理更高优先级的任务,然后再回来继续渲染
  • 优先级调度:不同的更新有不同的优先级,高优先级的更新先处理
  • Suspense:组件可以"等待"异步数据,在等待期间显示fallback
  • useTransition:标记非紧急更新,让UI保持响应

并发模式在React 17中还是实验性的,需要手动开启。React 18会正式发布并发模式。

2. React 17为并发模式做了哪些准备

React 17虽然没有正式发布并发模式,但做了很多底层改进:

  • 事件系统重构:新的事件委托方式,让事件处理可以更好地和并发模式配合
  • 渲染架构优化:优化了Fiber架构,为可中断渲染打下基础
  • 自动批处理改进:React 17改进了自动批处理(automatic batching),更多的更新会被批量处理,减少渲染次数
  • Suspense改进:改进了Suspense的行为,为未来的并发模式下的Suspense做准备

3. 如何为并发模式做准备

虽然并发模式还没正式发布,但现在就可以开始为它做准备:

  • 保持组件的纯粹性:并发模式下,渲染可能会被中断和重新执行,所以渲染函数应该是纯粹的,没有副作用。不要在render中修改外部变量、调用API等
  • 正确使用useEffect:副作用放在useEffect中,不要放在渲染过程中
  • 避免在渲染中使用ref:ref在并发模式下可能会有问题,尽量在useEffect中使用ref
  • 测试严格模式:开启严格模式(StrictMode),它会检测不安全的生命周期和副作用,帮助你发现并发模式下可能有问题的代码
  • 关注useTransition和useDeferredValue:了解这些新的Hook,为未来使用并发模式做准备

4. 最佳实践

  • 现在就开启严格模式:严格模式能帮你发现很多潜在问题,是为并发模式做准备的最好方式
  • 重构有副作用的渲染函数:检查你的组件,确保渲染函数没有副作用
  • 不要用UNSAFE_前缀的生命周期:这些生命周期在并发模式下会有问题,尽快迁移
  • 关注React团队的更新:并发模式还在快速发展,关注React团队的博客和更新,及时了解最新进展

我在项目中开启了严格模式,发现了几个有副作用的渲染函数,重构之后,代码更健壮了,也为未来的并发模式做好了准备。

六、升级注意事项

升级到React 17,有一些需要注意的地方。

1. 渐进式升级

React 17支持渐进式升级。也就是说,你可以在一个应用中同时使用React 16和React 17,逐步迁移。

渐进式升级的方式:

  • 用React 17的新根API(ReactDOM.createRoot)创建新的React 17应用
  • 老的部分继续用React 16的ReactDOM.render
  • 两部分可以通过DOM事件或全局状态通信

渐进式升级适合大型项目,可以降低升级风险。但对于中小型项目,建议一次性升级,更简单。

2. 依赖兼容性

升级前,要检查项目依赖的第三方库是否兼容React 17。

大部分主流库(比如Redux、React Router、Ant Design、Material UI)都已经兼容React 17。但一些比较老的、维护不活跃的库可能还有问题。

升级前,可以:

  • 查看库的GitHub Issues,看有没有人报告React 17的兼容性问题
  • 先在分支上升级,跑测试,看看有没有问题
  • 如果有兼容性问题,可以考虑升级库的版本,或者替换库

3. 测试

升级后,要充分测试。重点测试:

  • 事件相关的功能(onScroll、onFocus、onBlur等)
  • 错误边界的行为
  • 表单输入和受控组件
  • 第三方组件的行为
  • 性能(React 17应该不会变慢,但要确认)

建议有自动化测试的项目,先跑一遍测试,再手动测试关键功能。

4. 构建工具升级

升级React 17,可能需要升级构建工具:

  • Babel:升级@babel/preset-react到7.9.0以上
  • TypeScript:升级到4.1以上(如果要用新的JSX转换)
  • ESLint:升级eslint-plugin-react到最新版本
  • Webpack/Vite:一般不需要特别升级,但要确认兼容

5. 最佳实践

  • 先在开发环境升级测试:不要直接在生产环境升级,先在开发环境测试,确认没问题再发布
  • 保留回滚方案:升级前做好备份,保留回滚的方案,万一有问题可以快速回滚
  • 逐步启用新特性:不要一次性启用所有新特性,先启用最稳定的(比如新的JSX转换),再逐步启用其他
  • 关注React 17的更新日志:仔细阅读React 17的更新日志,了解所有的变更和注意事项

七、性能优化建议

升级到React 17后,可以结合新特性做一些性能优化。

1. 利用自动批处理

React 17改进了自动批处理。在React 16中,只有React事件处理函数中的更新会被批量处理。在React 17中,Promise、setTimeout、原生事件处理函数中的更新也会被批量处理。

这意味着,你不需要再手动用unstable_batchedUpdates来批量处理更新了。React会自动帮你批量处理,减少渲染次数。

如果你的代码中有手动调用unstable_batchedUpdates的地方,在React 17中可以移除了(大部分情况下)。

2. 合理使用React.memo

React.memo可以避免不必要的重新渲染。在React 17中,配合新的JSX转换和自动批处理,React.memo的效果更好。

建议:

  • 对渲染开销大的组件使用React.memo
  • 确保传给组件的props是稳定的(用useMemo、useCallback)
  • 不要滥用React.memo,对于简单的组件,memo的开销可能大于收益

3. 代码分割

React 17和React.lazy、Suspense配合,可以更好地做代码分割。

建议:

  • 用React.lazy做路由级别的代码分割
  • 对于大的组件,也可以用React.lazy做组件级别的代码分割
  • 配合Suspense的fallback,提供更好的加载体验

4. 利用新的DevTools分析性能

React 17配合最新的React DevTools,可以更好地分析性能。新版DevTools提供了:

  • 组件渲染时间的分析
  • 渲染原因的追踪
  • 提交(commit)的详细信息
  • 交互追踪(Interaction Tracking)

用DevTools分析应用的性能,找到渲染瓶颈,针对性优化。

八、写在最后

React 17虽然没有太多面向开发者的新API,但它是一个重要的版本。它的底层改进,为未来的并发模式打下了基础,也解决了一些长期存在的问题。

新的JSX转换,让代码更简洁,也让构建更高效。事件系统的改进,让React的事件处理更正确,也更容易和其他库集成。并发模式的准备,让我们可以提前为未来做准备。

升级到React 17,总体来说是比较平滑的。大部分项目升级后不需要做太多改动,就能享受React 17的改进。但也要注意一些breaking change,尤其是事件系统的变化,要充分测试。

我在几个项目中升级到React 17之后,整体体验不错。新的JSX转换让代码更干净,事件系统的改进解决了一些老问题,严格模式帮我发现了一些潜在问题。虽然并发模式还没正式发布,但为它做准备的过程,也让我的代码更健壮了。

React的发展很快,从Hooks到并发模式,React一直在进化。作为开发者,我们要保持学习,跟上React的发展,充分利用新特性,写出更好的代码。

最后,用一句话结束本文:"React 17是垫脚石,也是新起点。升级它,不仅是为了现在的改进,更是为了未来的可能。"愿每一个React开发者,都能顺利升级到React 17,在React的世界里写出更优雅、更高效的代码。