在迁移中学习 React 18:一份来自 React 17 的升级问题清单

前言

最近对公司的项目做了从React17到React18的升级,升级操作这里便不再介绍,官方文档都有。本文记录下升级到React18有哪些变动点。

TypeScript类型变动

1. FC 中不再有 children 类型

原先:

js 复制代码
interface modalContentProps {
  id?: string;
  [key: string]: string | ReactNode;
}

现在:

js 复制代码
interface modalContentProps {
  id?: string;
  children?: ReactNode; //需手动指定children
  [key: string]: string | ReactNode;
}

2. FC 中不再包含 ref 的类型定义

原先:

js 复制代码
interface Props {
  ref: ForwardedRef<any>;
  line: number;
}

const Single: FC<Props> = forwardRef(({ line }, ref) => {})

现在:

js 复制代码
interface Props {
  line: number;
}

const Single = forwardRef<ForwardedRef<any>, Props>(({ line }, ref) => {});

3. React.Key 新增 bigint 类型

原先React.key的类型为:

js 复制代码
string | number

现在:

js 复制代码
string | number | bigint

4. ReactNode 类型移除了对 DOM Element 的直接支持

原先:

js 复制代码
// ✅ React 17 中,Element 可以赋值给 ReactNode
const element: Element = document.createElement('div');
const reactNode: React.ReactNode = element; // ✅ 编译通过

现在:

js 复制代码
// ❌ React 18 中,Element 不能赋值给 ReactNode
const element: Element = document.createElement('div');
const reactNode: React.ReactNode = element; // ❌ 类型错误

行为变动

1. 并发模式与同步模式

使用新的API createRoot 开启并发模式:

js 复制代码
import ReactDOM from 'react-dom/client';

ReactDOM.createRoot(document.getElementById('root')).render(<App />);

在React 18中,依旧可以使用同步模式:

js 复制代码
import ReactDOM from 'react-dom';

ReactDOM.render(<App />, document.getElementById('root'));

2. defaultProps属性会告警提示

原先:

js 复制代码
SingleTagModal.defaultProps = {
  title: '标签',
};

现在使用默认参数:

js 复制代码
SingleTagModal = ({ title = '标签' }) =>{}

3. @testing-library/react-hooks弃用,合并到@testing-library/react

@testing-library/react从13版本开始支持React18的并发模式,此时@testing-library/react-hooks 的核心功能(主要是 renderHook)已内置到@testing-library/react从13中

原先:

js 复制代码
import { renderHook } from '@testing-library/react-hooks'

现在:

js 复制代码
import { renderHook } from '@testing-library/react'

4. 自动批处理

React 17同步模式:在React合成事件中,setState 的更新会被批处理。在JS原生事件、宏任务、微任务的情况下,setState 的更新不会被批处理。

js 复制代码
//同步模式
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 会渲染两次,每次更新一个状态(没有批处理)
}, 1000);

React 18开启并发模式后,所有的更新都会自动批量处理。

js 复制代码
//并发模式
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 最终,React 将仅会重新渲染一次(批处理)
}, 1000);

示例:比较常用的ahooks的useRequest,常常在onSuccess回调中处理请求成功后的行为

useRequest部分源代码:

js 复制代码
// 请求之前,将loading置为true
this.setState({
  loading: true,
  params,
  ...state,
});

// 处理请求
const res = await servicePromise;

//请求结束,将loading置为false
this.setState({
  data: res,
  error: undefined,
  loading: false,
});

//执行onSuccess
this.options.onSuccess?.(res, params);

await后面的任务为微任务,因此同步模式与并发模式的执行结果不一样

示例:

js 复制代码
import { useState } from 'react';

import { useRequest } from 'ahooks';

const getData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1);
    }, 1000);
  });
};

export default function User() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  const { loading } = useRequest(getData, {
    onSuccess: () => {
      setCount(count + 1);
      setFlag(!flag);
    },
  });

  console.log('渲染', count, flag, loading);

  return <span> {loading ? 'loading' : count}</span>;
}

同步模式下打印结果:loading置为false后,又分别渲染了两次,分别将count置为1和flag置为true

js 复制代码
渲染 0 false true
渲染 0 false true
渲染 0 false false
渲染 1 false false
渲染 1 true false

并发模式下打印结果:loading置为false,与count,flag的更新是一起的

js 复制代码
渲染 0 false true
渲染 0 false true
渲染 1 true false

自动批处理是一个很重要的变动点,很多升级之后带来的业务bug,都是该原因导致

5. Suspense 内的生命周期

在函数组件中的表现

同步模式:当一个树重新挂起并恢复时,不会触发任何生命周期钩子

并发模式:当一个树重新挂起时,触发useLayoutEffect的cleanup,重新恢复时,触发useLayoutEffect

在类组件中的表现

同步模式:当一个树重新挂起时,不会触发任何生命周期钩子;重新恢复时,触发componentDidUpdate

并发模式:当一个树重新挂起时,触发componentWillUnmount,重新恢复时,触发componentDidMount

对于使用react-activation实现KeepAlive功能的项目,这也是一个特别重要的变动点,react-activation本质是借助Suspense实现组件的冻结。在React 18的并发模式下,该库是有bug的。

因为componentWillUnmount被触发,导致子KeepAlive组件被误删,可通过调整生命周期的方式解决该bug。还有个问题就是useLayoutEffect触发机制调整,而antd4的动画效果是在useLayoutEffect触发,会有抖动等bug的发生,可改写rc-motion库。

新增的API与Hook

1. flushSync

  1. 退出批量更新
js 复制代码
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 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。

  1. flushSync 确保了在下一行代码运行时,React 已经更新了 DOM
js 复制代码
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;

当遇到React17中没有,但在18中出现的难以解决的问题时,优先考虑使用flushSync,而非setTimeout

2. startTransition

可以让你在不阻塞 UI 的情况下更新 state

js 复制代码
import { startTransition } from 'react';

// 紧急更新: 显示输入的内容
setInputValue(input);

// 将任何内部的状态更新都标记为过渡更新
startTransition(() => {
  // 过渡更新: 展示结果
  setSearchQuery(input);
});

3. useTransition

可以让你在不阻塞 UI 的情况下更新 state,比startTransition多个Pending状态

js 复制代码
const [ isPending , startTransition ] = useTransition ()

4. useDeferredValue

让开发者延迟更新UI的某些部分。可以延迟渲染不紧急的部分,类似于防抖但没有固定的延迟时间。

js 复制代码
  const [value, setValue] = useState("");
  const deferredValue = useDeferredValue(value);

5. useId

支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不兼容

js 复制代码
const id = useId()

6. useSyncExternalStore

用于订阅外部数据源的变化,并确保React组件能够同步地响应这些变化。大部分情景是用来处理Redux、MobX等外部状态管理库的数据订阅。

js 复制代码
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

7. useInsertionEffect

useInsertionEffect是一个专为CSS-in-JS库的开发者打造的钩子,比useLayoutEffect执行时机更早。

js 复制代码
useInsertionEffect(setup, deps?)

结尾

如果还有其他比较重要的变动点,欢迎在评论区留言!!!

参考文档:

zh-hans.react.dev/blog/2022/0...

zh-hans.react.dev/blog/2022/0...

相关推荐
顾安r6 小时前
12.17 脚本工具 自动化全局跳转
linux·前端·css·golang·html
踢球的打工仔6 小时前
jquery的基本使用(2)
前端·javascript·jquery
DEMO派6 小时前
前端javascript如何实现阅读位置记忆【可运行源码】
前端
苏打水com6 小时前
第十七篇:Day49-51 前端工程化进阶——从“手动”到“自动化”(对标职场“提效降本”需求)
前端·javascript·css·vue.js·html
文心快码BaiduComate6 小时前
Comate强力赋能:「趣绘像素岛」从体验泥潭到高性能可用的蜕变之路
前端·后端·程序员
『 时光荏苒 』6 小时前
使用Vue播放M3U8视频流的方法
前端·javascript·vue.js
Apifox6 小时前
Apifox + AI:接口自动化测试的智能化实践
前端·后端·测试
Tjohn96 小时前
前后端分离项目(Vue-SpringBoot)迁移记录
前端·vue.js·spring boot
CaoLv6 小时前
无需后端!用 React + WebLLM 把大模型装进浏览器,手撸一个“有脾气”的 AI 机器人 🤖
前端