如何在react中处理报错

news2024/12/23 22:57:02

本文为 360 奇舞团前端工程师翻译 

原文地址:https://www.developerway.com/posts/how-to-handle-errors-in-react

    我们都希望我们的应用能稳定、完美运行,并且能够考虑到每一个边缘情况。但是现实情况是,我们是人,是人就会犯错,并且也不存在没有bug的代码。无论我们多么小心或者编写了多少自动化测试,总会有出现严重错误的情况。重要的是,当错误影响到用户体验时,尽可能地定位它,并能以优雅的方式处理它,直到它真正被修复。
     所以今天,让我们来看看React中的错误处理:如果发生错误,我们可以做什么,不同的错误捕捉方法的注意事项是什么,以及如何减小错误的影响。

为何要捕获react中的错误

     那么第一件事:为什么在React中拥有一些错误捕获解决方案是极其重要的?
    这个答案很简单:从16版开始,在React生命周期中抛出的错误,如果不停止的话,将导致整个应用自行卸载。在此之前,组件会被保留在屏幕上,即使是样式错误和交互错误的。现在,在UI的一些无关紧要的部分,甚至是一些你无法控制的外部库中,一个未被捕获的错误也可以使整个页面挂掉,给用户呈现白屏。
    在此之前,前端开发人员从来没有过这样的破坏力。

还记得在js中是如何捕获错误信息的吗?

    在javascript里如何捕获错误?众所周知,我们可以使用try/catch语句:在try里做一些事情,在它们执行失败的时候catch这些错误来减少影响

try {
  // if we're doing something wrong, this might throw an error
  doSomething();
} catch (e) {
  // if error happened, catch it and do something with it without stopping the app
  // like sending this error to some logging service
}

相同的语法也适用于async函数

try {
  await fetch('/bla-bla');
} catch (e) {
  // oh no, the fetch failed! We should do something about it!
}

    如果我们正在使用旧的promises规范,它有专门的方法来捕获错误。我们可以基于promise的API来重写fetch例子,像下面这样:

fetch('/bla-bla').then((result) => {
  // if a promise is successful, the result will be here
  // we can do something useful with it
}).catch((e) => {
  // oh no, the fetch failed! We should do something about it!
})

    以上两种是相同的概念,只是实现方式稍有不同,因此在接下来的文章中,我将只对try/catch错误使用语法。

在React中的try/catch:如何操作和注意事项

    当一个错误被捕获时,我们需要对它做些什么?除了把它记录在某个地方之外,我们还能做什么?更准确地说:我们能为我们的用户做什么?仅仅让他们面对一个空屏幕或者一个不友好的界面。
    最明显和最直观的答案是在等待我们修复的时候渲染一些东西。幸运的是,我们可以在这个catch语句中做任何我们想做的事情,包括设置状态。所以我们可以做一些事情像这样:

const SomeComponent = () => {
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    try {
    } catch(e) {
      setHasError(true);
    }
  })

  if (hasError) return <SomeErrorScreen />

  return <SomeComponentContent {...datasomething} />
}

    我们试图发送一个获取请求,如果请求失败了--设置错误状态,如果错误状态为真,那么我们就渲染一个错误反馈的UI,为用户提供一些额外的信息,比如支持联系号码。
    这种方法非常简单,非常适合简单、可预测且范围狭窄的用例,例如捕获失败的fetch请求。
    但是如果你想捕捉一个组件中可能发生的所有错误,你将面临一些挑战和严格的限制。

限制1:你会在使用useEffect钩子时遇到困难

如果我们用try/catch包住useEffect,hook就失效了。

try {
  useEffect(() => {
    throw new Error('Hulk smash!');
  }, [])
} catch(e) {
  // useEffect throws, but this will never be called
}

    发生这种情况是因为useEffect是在渲染后被异步调用的,所以从try/catch的角度来看,一切都很顺利。这和任何Promise都是一样的:如果我们不等待结果,那么javascript就会继续它的工作,在承诺完成后返回,并且只执行useEffect(或Promise)中的内容。
    为了让在useEffect中的错误被捕获,try/catch应该被放在里面。

useEffect(() => {
  try {
    throw new Error('Hulk smash!');
  } catch(e) {
    // this one will be caught
  }
}, [])

    看一下这个例子就知道了:https://codesandbox.io/s/try-catch-and-useeffect-28h3ux?from-embed
    这适用于所有使用useEffect的钩子或异步事情的场景。因此,你不能用一个try/catch包裹所有代码,而是将其拆分到每个hook中

限制2:子组件。

try/catch不能捕捉子组件内发生的任何事情。你不能像下面这样做:

const Component = () => {
  let child;

  try {
    child = <Child />
  } catch(e) {
    // useless for catching errors inside Child component, won't be triggered
  }

  return child;
}

甚至是这样

const Component = () => {
  try {
    return <Child />
  } catch(e) {
    // still useless for catching errors inside Child component, won't be triggered
  }
}

    可以看一下这个例子https://codesandbox.io/s/try-catch-for-children-doesnt-work-5elto1?from-embed
    发生这种情况是因为当我们写时,我们实际上并没有渲染这个组件。我们所做的是创建一个组件元素,这只是一个组件的定义。它只是一个包含必要信息的对象,比如组件类型和道具,以后会被React本身使用,它将实际触发这个组件的渲染。它将在try/catch块成功执行后发生,与promises和useEffect钩子的情况完全一样。
    如果你想更详细地了解元素和组件的工作原理,这里有一篇文章适合你:React元素、子代、父代和重排的奥秘(https://www.developerway.com/posts/react-elements-children-parents)

限制3:在渲染过程中设置state是不可取的

    如果你想在useEffect和各种回调之外捕获错误(也就是说在组件的渲染过程中),那么正确处理它们就不再简单了,因为渲染过程中的状态更新是允许的。
比如像这样简单的的代码,如果发生错误,就会导致重新渲染无限循环。

const Component = () => {
  const [hasError, setHasError] = useState(false);

  try {
    doSomethingComplicated();
  } catch(e) {
    setHasError(true);
  }
}

当然,我们可以在这里直接返回错误组件,而不是设置错误状态。

const Component = () => {
  try {
    doSomethingComplicated();
  } catch(e) {
    return <SomeErrorScreen />
  }
}

但是,正如你想的,这有点麻烦,而且会迫使我们对同一组件的错误进行不同的处理:对useEffect和回调进行状态处理,而对其他的直接返回错误组件

const SomeComponent = () => {
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    try {
      // do something like fetching some data
    } catch(e) {
      setHasError(true);
    }
  })

  try {
    // 
  } catch(e) {
    return <SomeErrorScreen />;
  }

  if (hasError) return <SomeErrorScreen />

  return <SomeComponentContent {...datasomething} />
}

    总结一下本节的内容:如果在React中仅仅依靠try/catch,要么会错过大部分的错误,要么会把每个组件变成难以理解的混乱代码而造成错误
幸运的是,还有其他方法。

React ErrorBoundary component

    为了减轻上面的限制,React给我们提供了“错误边界”:一种特殊的API,它以某种方式将普通组件转换为 try/catch 语句,但是仅适用于 React 声明的代码。你可以在下面的示例中看到的经典用法,包括 React 文档。

const Component = () => {
  return (
    <ErrorBoundary>
      <SomeChildComponent />
      <AnotherChildComponent />
    </ErrorBoundary>
  )
}

    现在,如果这些组件或者他们的子组件在渲染中出现错误,这个错误会被捕获并处理。
    但是React并没有提供原生组件,只是给我们提供了一个工具来实现它。最简单的实现是这样子的:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    // 初始化error的状态
    this.state = { hasError: false };
  }

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

  render() {
    if (this.state.hasError) {
      return <>Oh no! Epic fail!</>
    }

    return this.props.children;
  }
}

    我们创建了一个普通的类组件,并实现了getDerivedStateFromError方法--这个方法可以让组件拥有错误边界。
在处理错误时,另一个重要的事情是将错误信息传递到某个地方,让它能够触发所有监听者。为此,错误边界提供了componentDidCatch方法

class ErrorBoundary extends React.Component {
  componentDidCatch(error, errorInfo) {
    log(error, errorInfo);
  }
}

在错误边界设置后,我们可以像使用其他组件一样使用它。比如,我们可以将其优化得更便于重用,并将fallback做为props来传递

render() {
  if (this.state.hasError) {
    return this.props.fallback;
  }

  return this.props.children;
}

可以像下面这样使用:

const Component = () => {
  return (
    <ErrorBoundary fallback={<>Oh no! Do something!</>}>
      <SomeChildComponent />
      <AnotherChildComponent />
    </ErrorBoundary>
  )
}

    或者其他我们可能需要的东西,比如点击按钮时重置状态,区分错误类型,或者将错误传递到某个上下文中。查看完整的例子:https://4ldsun.csb.app/
不过,这里有一个注意事项:它并不能捕获一切错误

错误边界组件的限制

    错误边界只捕捉发生在React生命周期中的错误。在生命周期之外发生的事情,如resolved promise、带有setTimeout的异步代码、各种回调和事件监听函数,如果没有被不明确处理,将不能捕获。

const Component = () => {
  useEffect(() => {
    throw new Error('Destroy everything!');
  }, [])

  const onClick = () => {
    throw new Error('Hulk smash!');
  }

  useEffect(() => {
    fetch('/bla')
  }, [])

  return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
  return (
    <ErrorBoundary>
      <Component />
    </ErrorBoundary>
  )
}

    这里的建议是使用常规的try/catch来处理这类错误。而且至少在这里我们可以安全地使用state:事件监听函数的回调正是我们通常setState的地方。所以从技术上讲,我们可以把两种方法结合起来,做这样的事情。

const Component = () => {
  const [hasError, setHasError] = useState(false);
  const onClick = () => {
    try {
      throw new Error('Hulk smash!');
    } catch(e) {
      setHasError(true);
    }
  }

  if (hasError) return 'something went wrong';

  return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
  return (
    <ErrorBoundary fallback={"Oh no! Something went wrong"}>
    <Component />
    </ErrorBoundary>
  )
}

    但是。我们又回到了原点:每个组件都需要维持它的 "错误 "状态,更重要的是--决定如何处理它。
    当然,我们可以不在组件层面上处理这些错误,而只是通过props或Context将它们传递到拥有ErrorBoundary的父级。这样的话,我们只需要在一个地方设置一个 "fallback"组件。

const Component = ({ onError }) => {
  const onClick = () => {
    try {
      throw new Error('Hulk smash!');
    } catch(e) {
      onError();
    }
  }

  return <button onClick={onClick}>click me</button>
}

const ComponentWithBoundary = () => {
  const [hasError, setHasError] = useState();
  const fallback = "Oh no! Something went wrong";

  if (hasError) return fallback;

  return (
    <ErrorBoundary fallback={fallback}>
    <Component onError={() => setHasError(true)} />
    </ErrorBoundary>
  )
}

    但这里有很多冗余代码,我们必须对渲染树的每一个子组件都这样做。更不用说我们现在还要维护两个错误状态:父组件,以及ErrorBoundary本身。而ErrorBoundary已经实现了一套捕获错误的机制,我们在这里做了重复的工作。
那么,我们就不能用ErrorBoundary从异步代码和事件处理程序中捕捉这些错误吗?

ErrorBoundary捕捉异步错误

    有趣的是--我们可以用ErrorBoundary把它们都捕获!大家最喜欢的Dan Abramov与我们分享了一个很酷的黑客技术。正是为了实现这一点:Throwing Error from hook not caught in error boundary · Issue #14981 · facebook/react. (https://github.com/facebook/react/issues/14981#issuecomment-468460187)
    这里的技巧是先用try/catch捕捉这些错误,然后在catch语句中触发正常的React重渲染,然后把这些错误重新抛回重渲染的生命周期。这样,ErrorBoundary就可以像捕获其他错误一样捕捉它们。由于state变化是触发重新渲染的方式,而setState实际上可以接受一个更新函数作为参数,这个解决方案是纯粹的黑魔法。

const Component = () => {
  const [state, setState] = useState();

  const onClick = () => {
    try {
      // something bad happened
    } catch (e) {
      setState(() => {
        throw e;
      })
    }
  }
}

    完整例子在这里:https://codesandbox.io/s/simple-async-error-in-error-boundary-r8l22g?from-embed
    这里的最后一步将其抽象化,所以我们不必在每个组件中创建随机状态。我们可以在这里发挥创意,实现一个钩子用来将异步错误抛出。

const useThrowAsyncError = () => {
  const [state, setState] = useState();

  return (error) => {
    setState(() => throw error)
  }
}

像这样使用它:

const Component = () => {
  const throwAsyncError = useThrowAsyncError();

  useEffect(() => {
    fetch('/bla').then().catch((e) => {
      // throw async error here!
      throwAsyncError(e)
    })
  })
}

或者,我们可以像下面这样为回调做一层封装:

const useCallbackWithErrorHandling = (callback) => {
  const [state, setState] = useState();

  return (...args) => {
    try {
      callback(...args);
    } catch(e) {
      setState(() => throw e);
    }
  }
}

像下面这样使用它:

const Component = () => {
  const onClick = () => {
    // do something dangerous here
  }

  const onClickWithErrorHandler = useCallbackWithErrorHandling(onClick);

  return <button onClick={onClickWithErrorHandler}>click me!</button>
}

完整的例子在这里:https://codesandbox.io/s/simple-async-errors-utils-for-error-boundary-fzg5zv?from-embed

可以用 react-error-boundary 来代替吗?

    对于那些讨厌重新造轮子的人,或者喜欢用库来解决已经解决的问题的人,有一个很好的库,它实现了一个灵活的ErrorBoundary组件,并且有一些类似于上述的有用的工具:https://github.com/bvaughn/react-error-boundary
    是否使用它,只是个人喜好、编码风格和组件特殊性的问题。
    今天就说到这里,希望从现在开始,当你的应用程序发生了报错,你都能够轻松而优雅地处理这些情况。
请记住:

  • try/catch块不会捕获像useEffect这样的钩子和任何子组件中的错误。

  • ErrorBoundary可以捕捉它们,但它不会捕捉异步代码和事件处理回调中的错误。

  • 不过,你可以让ErrorBoundary捕捉这些错误,你只需要先用try/catch捕捉它们,然后再把它们重新传递到React生命周期中。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

8c747467d45da49d05a9b7612134b758.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/418207.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【区块链】走进web3的世界-DApp如何快速接入wall

在web3中&#xff0c;wall是您进入区块链的一个标识&#xff0c;每个用户使用的wall都不近相同&#xff0c;因此接入更多的wall是很有必要的&#xff0c;从用户角度来说&#xff0c;非必要情况下&#xff0c;我是不愿意去额外下载wall的。因此今天我们来聊一下&#xff0c;DApp…

手把手教你搭建 Docker API 未授权漏洞环境

环境&#xff1a;ubuntu 16.04 我是在虚拟机中安装了ubuntu 16.04的环境&#xff0c;里面暂时没有docker、ssh远程、vim等。 1. 更换国内源 直接输入docker 这里没有安装&#xff0c;那安装下 sudo apt install docker.io 开始安装 因为我没有修改源&#xff0c;所以这个速…

数据的表示和存储1

目录 数制和编码 信息的二进制编码 数值数据的表示 进制计数制 定点数和浮点数 定点数的编码表示 原码表示 补码表示 求特殊数的补码 求真值的补码 求补码的真值 移码表示Excess (biased) notiion C语言中的整数 无符号整数(unsigned integer) 带符号整数&#xff0…

QT sql查询模型,视图显示

SQL模型定义定义sql查询模型查询设置格式创建视图并显示setHeaderData() 函数定义 SQL 查询模型&#xff08;QSqlQueryModel&#xff09;是 Qt 提供的一种数据模型&#xff0c;用于执行 SQL 查询并返回结果集。它继承自 QSqlTableModel 类&#xff0c;可以使用 QSqlQuery 对象执…

【eXtplorer】本地搭建免费在线文件管理器并实现在外远程登录

文章目录1. 前言2. eXtplorer网站搭建2.1 eXtplorer下载和安装2.2 eXtplorer网页测试2.3 cpolar的安装和注册3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置4.公网访问测试5.结语1. 前言 通过互联网传输文件&#xff0c;是互联网最重要的应用之一&#xff0c;无论是网上…

springboot学习2

一、spring boot自动装配原理 pom.xml spring-boot-dependencies 核心依赖在父工程中 在写或者引入一些spring boot依赖的时候&#xff0c;不需要指定版本&#xff0c;因为有这些版本仓库启动器 <dependency><groupId>org.springframework.boot</groupId>&…

使用golang实现日志收集系统的logagent

整体架构 参考 七米老师的日志收集项目 主要用go实现logagent的部分&#xff0c;logagent的作用主要是实时监控日志追加的变化&#xff0c;并将变化发送到kafka中。 之前我们已经实现了 用go连接kafka并向其中发送数据&#xff0c;也实现了使用tail库监控日志追加操作。 我们…

【Java基础】反射详述简单模拟SpringMVC

&#x1f6a9; 本文已收录至专栏&#xff1a;JAVA基础 &#x1f44d;希望能对你有所帮助 一.概述 反射是指对于任何一个Class类&#xff0c;在运行的时候都可以直接得到这个类全部成分&#xff0c;使得我们可以动态操作Java代码&#xff0c;同时反射也破坏了Java的封装性。 例…

如何清理电脑缓存,分享4个简单方法!

案例&#xff1a;如何清理电脑缓存 【朋友们&#xff01;我感觉我电脑的内存已经严重不足了&#xff0c;想清理下电脑的缓存释放空间&#xff0c;却不知如何清理&#xff0c;大家有什么好的方法吗&#xff1f;】 经常使用电脑的朋友可能都会发现&#xff0c;好像我们没下载什…

平台和编译器决定 char 是 signed char 或者 unsigned char

平台和编译器决定 char 是 signed char 或者 unsigned charThe C and C standards allows the character type char to be signed or unsigned, depending on the platform and compiler. Most systems, including x86 GNU/Linux and Microsoft Windows, use signed char, but …

NetCore3.1或Net6.0项目升级到Net7.0

其实与我之前发布的步骤基本一致&#xff0c;升级到net6.0之后&#xff0c;在升级net7.0基本没有可修改的代码&#xff0c;只是升级一些nuget包而已&#xff0c;NetCore3.1升级到Net6.0&#xff0c;可参考此文章&#xff1a;NetCore3.1项目升级到Net6.0_csdn_aspnet的博客-CSDN…

Rust China Conf 2023 筹备启动:议题征集开始

大会介绍Rust China Conf 2023 由 Rust 中文社区发起主办、知名企业和开源组织联合协办&#xff0c;是年度国内规模最大并唯一的 Rust 线下大型会议&#xff0c;深受 Rust 中文社区开发者与相关企业的喜爱与推崇。本次大会为线下会议&#xff0c;将于6月17日-18日在上海举办&am…

Java虚拟机总结

前言 Java是目前用户最多、使用范围最广的软件开发技术之一。Java的技术体系主要由支撑Java程序运行的虚拟机、提供各开发领域接口支持的Java APl、,Java编程语言及许多第三方Java框架&#xff08;如Spring、Struts等&#xff09;构成。在国内&#xff0c;有关Java APl、Java语…

【蓝桥杯嵌入式】第十四届蓝桥杯嵌入式省赛[第一场]程序设计题以及详细题解

文章目录原题展示原题分析原题题解LED相关LCD相关按键相关ADC相关定时器相关PWM输入捕获小结文章福利原题展示 原题分析 今年的第一场比赛绝对np,官方将串口直接省掉了&#xff0c;将其替换成很多小功能&#xff0c;如&#xff1a;切换计时、频率均匀变化、锁机制等等&#xff…

Angular 全屏后选择器 (nz-select) 下拉选项框失效【开发笔记】

问题&#xff1a;Angular 全屏后选择器 (nz-select) 下拉选择无法使用 如图&#xff1a; 相应解决方法的文章&#xff1a;https://medium.com/shahar.kazaz/adding-fullscreen-support-to-ng-zorro-a38140da676 三种解决方法&#xff1a; ① FullscreenOverlyContainer&#…

SAP中用CS20批量修改BOM应用问题处理实例

在应用中可能会遇到这样的情况&#xff0c;用户通过某个工艺或技术上的改进&#xff0c;节约了某个原料的用量&#xff0c;而这个原料可能应用在一批成品上。如果成品数量太大&#xff0c;就需要做批量的变更了。 CS20这个事务应该就是用于做BOM批量处理的&#xff0c;笔者之前…

C++基础回顾(上)

C基础回顾&#xff08;上&#xff09; 目录C基础回顾&#xff08;上&#xff09;前言关键字和标识符运算符数据类型函数类前言 C之前学过一点&#xff0c;但是很长时间都没用过&#xff0c;翻出了书从头看了一遍&#xff0c;简短地做了笔记&#xff0c;以便自己之后查看和学习…

5、存储引擎

1、查看存储引擎 查看mysql提供什么存储引擎&#xff1a; show engines;2、设置系统默认的存储引擎 查看默认的存储引擎&#xff1a; show variables like %storage_engine%; #或 SELECT default_storage_engine;修改默认的存储引擎 如果在创建表的语句中没有显式指定表的存…

教你精通Java语法之第十二章、递归

目录 一、递归 1.1递归的概念 1.1.1定义 1.1.2原理 1.1.3思路 1.2单路递归 1.2.1阶乘 1.2.2正向输出数字 1.2.3反向输出字符串 1.3多路递归 1.3.1斐波那契数列 1.3.2兔子问题 1.3.3青蛙爬楼梯 1.4汉诺塔问题 1.5猴子吃桃问题 1.6老鼠走迷宫问题 二、递归的时…

从视频中截取gif怎么弄?三步简单完成视频转gif制作

电影、电视剧等短视频充斥着我们的生活&#xff0c;很多小伙伴会将这些视频中的有趣画面提取出来做成Gif动画表情包。那么&#xff0c;怎么才能从视频中提取gif动画呢&#xff1f; 一、使用什么工具才能从视频中提取gif呢&#xff1f; 通过使用GIF中文网这款专业的视频转gif&…