首页 > 其他分享 >React 的 Suspense 和 ErrorBoundary 还有这种关系?

React 的 Suspense 和 ErrorBoundary 还有这种关系?

时间:2023-12-24 18:38:13浏览次数:34  
标签:return ErrorBoundary React Suspense 组件 promise throw

Suspense 组件想必大家都用过,一般是和 React.lazy 结合用,用来加载一些异步组件。

比如这样一个组件:

// src/Aaa.jsx
export default function Aaa() {
    return <div>aaa</div>
}

就可以在另一个组件里用 lazy + Suspense 异步加载:

import React, { Suspense } from 'react';

const LazyAaa = React.lazy(() => import('./Aaa'));

export default function App() {
  return <div>
    <Suspense fallback={'loading...'}>
      <LazyAaa></LazyAaa>
    </Suspense>
  </div>
}

React 的 Suspense 和 ErrorBoundary 还有这种关系?_JavaScript

这里的 import 是 webpack 提供的用来异步加载模块的 api,它会动态下载模块所在的 chunk,然后从中解析出该模块,拿到 export 的值:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_javascript_02

后台管理系统用这个挺多的,因为不可能一下子把所有路由的组件都下载下来,所以会用 lazy + Suspense 的方式异步加载一些组件。

大多数场景下,Suspense 就专门和 lazy 搭配使用的。

但有的时候,你会发现 Suspense 不搭配 lazy 也可以。

比如 jotai 这个状态管理库,它就号称支持了 Suspense,可以这样写:

import { Suspense } from 'react'
import { atom, useAtom } from 'jotai'

const userAtom = atom(async (get) => {
  const userId = 1;
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}?_delay=2000`
  )
  return response.json()
})

const UserName = () => {
  const [user] = useAtom(userAtom)
  return <div>User name: {user.name}</div>
}

export default function App() {
  return <Suspense fallback="Loading...">
    <UserName />
  </Suspense>
}

React 的 Suspense 和 ErrorBoundary 还有这种关系?_JavaScript_03

现在并不是用 lazy 异步加载组件呀,怎么触发的 Suspense 呢?

回答这个问题会涉及到 ErrorBoundary。

现在 react 官网都推荐用 function 而不是 class 了:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_App_04

绝大多数情况我们用 function 组件就好了,没必要用 class 组件。

但是有一个特性是只有 class 组件才有的,就是 ErrorBoundary。

这样写:

import { Component } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = {
      hasError: false
    };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, message: error.message };
  }

  componentDidCatch(error, errorInfo) {
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <div>出错了: {this.state.message}</div>;
    }
    return this.props.children;
  }
}

当子组件报错的时候,会把错误传递给它的 getDerivedStateFromError 和 componentDidCatch 方法。

getDerivedStateFromError 接收 error,返回一个新的 state,会触发重新渲染来显示错误对应的 UI。

componentDidCatch 接收 error 和堆栈 info,可以用来打印错误日志。

我们试一下:

function Bbb() {
  const b = window.a.b;

  return <div>{b}</div>
}

export default function App() {
  return <ErrorBoundary>
    <Bbb></Bbb>
  </ErrorBoundary>
}

window.a.b 不存在,所以正常情况下会报错,页面白屏。

但现在加上 ErrorBoundary 是这样的:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_JavaScript_05

getDerivedStateFromError 修改 state 触发重新渲染,渲染出错误对应的 UI。

componentDidCatch 拿到错误信息,打印日志。

这样,就对组件抛错的情况做了兜底。

这个特性只有 class 组件有,function 组件没有:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_javascript_06

不过一般也不用自己写这种 ErrorBoundary 组件,直接用 react-error-boundary 这个包就行:

npm install --save react-error-boundary

试一下:

import { ErrorBoundary } from "react-error-boundary";

function Bbb() {
  const b = window.a.b;

  return <div>{b}</div>
}

function fallbackRender({ error }) {
  return (
    <div>
      <p>出错了:</p>
      <div>{error.message}</div>
    </div>
  );
}

export default function App() {
  return <ErrorBoundary fallback={fallbackRender}>
    <Bbb></Bbb>
  </ErrorBoundary>
}

React 的 Suspense 和 ErrorBoundary 还有这种关系?_JavaScript_07

而且并不一定是 ErrorBoundary 的 children,任意层级的子组件都可以:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_React.js_08

React 的 Suspense 和 ErrorBoundary 还有这种关系?_App_09

也就是说组件抛错的时候,会向上寻找最近的 ErrorBoundary 组件。

这也是 boundary 的含义。

话说回来,为什么讲 Suspense 要扯到 ErrorBoundary 呢?

这俩有啥关系?

其实 Suspense 也是用 throw error 的方式实现的。

比如这样:

import { Suspense } from "react";

let data, promise;
function fetchData() {
  if (data) return data;
  promise = new Promise(resolve => {
    setTimeout(() => {
      data = '取到的数据'
      resolve()
    }, 2000)
  })
  throw promise;
}

function Content() {
  const data = fetchData();
  return <p>{data}</p>
}

export default function App() {
  return (
    <Suspense fallback={'loading data'}>
      <Content />
    </Suspense>
  )
}

可以看到,触发了 Suspense:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_前端_10

也就是说,只要 throw 一个 promise,就会被最近的 Suspense 捕获。

promise 初始状态展示 fallback,promise 改变状态后展示子组件。

那 React.lazy 是不是也是基于这个实现的呢?

调试下源码,发现确实是这样:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_JavaScript_11

React.lazy 包裹之后,也会 throw 一个 promise 来触发 Suspense。

当 promise 改变状态后,再返回拿到的值。

这样为什么 jotai 可以支持 Suspense 我们也就知道了:

也是这样实现的:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_React.js_12

有的同学可能会问了:ErrorBoundary 是捕获组件 throw 的错误,而 Suspense 是捕获组件 throw 的 promise,这俩会冲突么?

试一下就知道了:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_React.js_13

包裹一层 ErrorBoundary,你会发现没有触发:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_javascript_14

而 throw 一个 error 的时候:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_前端_15

ErrorBoundary 就触发了:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_前端_16

也就是说,ErrorBoundary 和 Suspense 虽然都是捕获组件 throw 出的东西,但这俩互不相干,一个捕获 error,一个捕获 promise。

大概看下源码的处理:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_App_17

首先会全部 catch,然后内部再区分两种情况:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_javascript_18

如果 throw 的是 error,就是 error boundary 的处理逻辑,找最近的一个 ErrorBoundary 组件来处理。

如果 throw 的是 promise,则是 suspense boundary 的处理逻辑,找最近的 Suspense 组件来处理。

两者互不相干。

但业务代码我们不用 Suspense 来写这种 loading。

大家都这么写:

import { useEffect, useState } from "react";

function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({
        name: 'guang'
      });
    }, 2000)
  })
}

export default function App() {
  const [loading, setLoading] = useState(false);
  const [data, setData] = useState({});


  async function load() {
    setLoading(true);
    const data = await fetchData();
    setData(data);
    setLoading(false);
  }

  useEffect(() => {
    load();
  }, []);

  return <div>
    { loading ? 'loding...' : data.name }
  </div>
}

React 的 Suspense 和 ErrorBoundary 还有这种关系?_React.js_19

就是加一个 state 来记录 loading 状态就行了。

要是用 Suspense,需要 throw 一个 promise 才行,那可太费劲了,而且代码也不好维护。

不过如果你用了一些支持 Suspense 的框架,比如 jotai、next.js 等,那也可以用 Suspense。

框架内部给你做了 throw promise 的事情:

import { Suspense } from 'react'
import { atom, useAtom } from 'jotai'

const userAtom = atom(async (get) => {
  const userId = 1;
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}?_delay=2000`
  )
  return response.json()
})

const UserName = () => {
  const [user] = useAtom(userAtom)
  return <div>User name: {user.name}</div>
}

export default function App() {
  return <Suspense fallback="Loading...">
    <UserName />
  </Suspense>
}

本来 Suspense 就是用来做这个的,结果现在只有 lazy 加载异步组件的时候才能用。

react 团队也在想办法解决这个问题,所以出了一个 use 的 hook:

这样用:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_App_20

它的参数是 promise。

当 promise 在 pending 的时候,展示 suspense 的 fallback。

当 promise 是 resolve 的时候,展示 Suspense 的子组件。

当 promise 是 reject 的时候,展示 ErrorBoundary 的 fallback。

React 的 Suspense 和 ErrorBoundary 还有这种关系?_JavaScript_21

这样就不用自己 throw promise 了,业务代码就可以用 Suspense 来 loading 了。

不过别高兴太早,这个 use 的 hook 还在实验阶段,还没正式发布。

我们刚才用的 jotai 就自己实现了一下 use:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_App_22

就是 pending 的时候 throw promise,reject 的时候 throw error,否则 return 数据。

等 use 这个 hook 正式发布了,大家就可以告别自己写个 state 标识 loading 状态这种方式了,直接用 Suspense。

这就是文档里写的触发 Suspense 的 3 种方式:

React 的 Suspense 和 ErrorBoundary 还有这种关系?_javascript_23

一种是用支持 Suspense 的框架,比如 next.js 或者 jotai。

一种是 lazy 异步加载组件。

再一种就是还在实验阶段的 use 了。

这些不同的方式底层都是 throw promise。

总结

大多数人用 Suspense 都是结合 React.lazy 异步加载组件的时候用,其实它也可以独立用。

它的底层原理就是 throw 一个 promise,然后 React 会捕获这个 promise,交给最近的 Suspense 组件来处理。

类似的,ErrorBoundary 也是这种处理方式,只不过捕获的是 throw 的 error。

ErrorBoundary 只能是 class 组件的形式,通过 getDerivedStateFromError 方法来接收错误修改 state,以及 componentDidCatch 来打印错误日志。

自己写 throw promise 来触发 Suspense 还是很麻烦的,一般我们都不用这个,而是自己写个 loading 的 state 来标识。

不过当你用 next.js、jotai 等框架的时候,因为内部做了 throw promise 的封装,就可以直接用 Suspense 了。

此外,react 有一个 use 的 hook,可以接收 promise,在 pending 的时候触发 Suspense,在 reject 的时候触发 ErrorBoundary,底层原理就是 throw error 和 promise。

这个 hook 还在实验阶段,等正式发布之后,估计代码里就会有大量 Suspense 了。

就像标题说的,Suspense 和 ErrorBoundary 看似是两种不同的东西,但其实不管是用法还是实现原理,都是很类似的。

标签:return,ErrorBoundary,React,Suspense,组件,promise,throw
From: https://blog.51cto.com/u_15506823/8956364

相关文章

  • React系列:react项目的创建到可以编写业务的一些列初始化
    ......
  • react Hooks+Context 实现响应式布局
    1.创建文件 viewportContext.tsx importReactfrom"react";constdefaultValue={  width:window.innerWidth}constviewportContext=React.createContext(defaultValue);constViewportProvider=({children})=>{  const[width,setWidt......
  • React 之使用 antd 实现全局 loading
    使用antd的Spin实现Loading代码如下:fullLoading.tsximportReactDOMfrom"react-dom/client";import{Spin}from"antd";//全局加载loading//当前请求的个数//当同时有多个请求时,等所有请求完成后再关闭loadingletreqCount=0;//显示loadingfuncti......
  • React项目中报错:Parsing error: The keyword 'import' is reservedeslint
    记得更改完配置后,要重启编辑器(如:VSCode)!!!记得更改完配置后,要重启编辑器(如:VSCode)!!!记得更改完配置后,要重启编辑器(如:VSCode)!!!这个错误通常发生在你尝试在一个不支持ES6模块语法的环境中使用import关键字。ESLint默认使用的是ES5语法,如果你想使用ES6或者更新的语法,你......
  • React Hooks的使用规范和最佳实践
    ReactHooks自从推出以来,彻底改变了React组件的编写方式。它们提供了一种在函数组件中使用state和其他React特性的能力,从而使得函数组件更加强大和灵活。本文将深入探讨useEffect、useMemo、useCallback和useState这四种常用Hooks的特点、优缺点,以及它们对组件性能的影响。我们还......
  • 100道React高频题整理(附答案背诵版)
    1、简述React有什么特点?React是一个用于构建用户界面的JavaScript库,由Facebook开发并维护。React有以下几个主要特点:声明式设计:React采用声明式设计,让代码更易于理解,且方便调试。你只需描述出你希望程序的最终状态,React会自动确保用户界面与你描述的状态保持一致。组件化:......
  • 让你 React 组件水平暴增的 5 个技巧
    让你React组件水平暴增的5个技巧神说要有光​ ​关注他 你经常看TA的内容最近看了一些AntDesign的组件源码,学到一些很实用的技巧,这篇文章来分享一下。首先,我们用create-react-app创建个React项目(选择typescript模版):npxcreate-r......
  • #yyds干货盘点#在 React Router 中使用 JWT
    创建一个React项目使用下方的指令会为我们创建一个项目$npmcreatevite@latestreact-jwt-cn然后我们选择 react 和 javascript 作为我们的框架和语言。在项目开始之前,我们要确保所有的依赖都已经被安装,所以我们要先执行$npminstall安装完毕后,在项目的根目录下,我们可以运......
  • React useEffect 在组件挂载时运行了两次
    在使用useEffect这个hook时,发现useEffect会执行2次useEffect(()=>{console.log("执行useEffect");},[]);控制台输出:执行useEffect执行useEffect查看疑难解答官方文档,官方解释如下:在开发环境下,如果开启严格模式,React会在实际运行setup之前额外运行一次......
  • 使用React+SpringBoot开发一个协同编辑的表格文档
    前言随着云计算和团队协作的兴起,协同编辑成为了许多企业和组织中必不可少的需求。通过协同编辑,多个用户可以同时对同一个文档进行编辑和更新,从而提高工作效率和协作能力。本文小编就将为大家介绍如何使用React+SpringBoot简单的开发一个协同编辑的表格文档。环境准备用到的开发......