我们如何减少React代码库中的错误

116 阅读7分钟

[

Darshita Chaturvedi

](medium.com/@darshitac_…)

Darshita Chaturvedi

关注

6月13日

-

6分钟阅读

[

拯救

](medium.com/m/signin?ac…)

我们如何减少React代码库中的错误

了解React中的模式和反模式

照片:SurfaceonUnsplash

简介

最近,在处理我们的大型React应用代码库时,我们遇到了三类bug--它们不是编译时或运行时的错误,而是意外的代码行为。

  • 一个组件在用户事件发生时没有更新。
  • 一个组件在用户事件发生时部分更新。
  • 一个组件出乎意料地显示出来。

我们的第一直觉当然是 "在我们能找到的地方与邪恶战斗"。

然而,即使经过一连串的打印语句,这些错误仍然难以追踪。这时我们才意识到,我们代码的某些部分可以被认为是反模式的。

因此,我们花了很多时间来理解和描述它们,以确保我们在未来避免这些错误。

这篇文章是对这些发现的解释。

React中的模式和反模式

在这篇文章中,如果一个React代码符合以下条件,就可以成为一个好的模式。

  • 该组件是可重复使用的。
  • 代码更容易审查和调试。

请注意,如果我们写了更多行的代码,或者为了实现上述目标,我们(预计)引入了一些额外的渲染,那么该代码仍然被认为是一种模式。

为什么即使是有经验的开发者也会落入反模式的陷阱?

  1. React代码在遵循模式的时候与遵循反模式的时候看起来惊人的相似。
  2. 模式看起来如此明显,以至于它被忽略了。

如何识别反模式?

提示#1:没有依赖阵列的钩子

在React中,不同的代码片断通过依赖关系相互连接。这些相互依赖的代码片断在一起使应用程序的状态保持在所需的形式。因此,如果我们在React中写了一段没有依赖关系的代码,很有可能会导致bug的出现。

因此,当你使用钩子时要谨慎,如useStateuseRef 等,因为它们不采取依赖关系数组。

man yelling at another. caption: and when i open the code, what do i find? there is no dependency array with this hook!

来源:imgflip.com

提示#2:嵌套而不是组合

有两种机制来安排React组件。

  1. 组成。所有的孩子都有相同的数据
  2. 嵌套。每个孩子都可以有不同的数据

图片由作者提供

让我们想象一下这样一个场景:我们观察到 "子3 "中存在一个错误。

如果我们使用组合方式排列组件,我们就不必查看 "子1 "和 "子2 "的代码了,因为它们都是独立的。因此,调试的时间复杂性将是O (1)

然而,如果我们使用嵌套的方式排列组件,我们就必须检查 "子3 "之前的所有子组件,以找出错误的根源。在这种情况下,调试的时间复杂度将是O (n) ,其中n 是 "孩子3 "上面的孩子的数量。

因此,我们可以得出结论,嵌套往往比组合更难调试过程。

应用实例

现在,让我们考虑一个应用程序来展示不同的模式和反模式。

该应用程序的期望行为

当在左边的导航菜单中点击一篇文章时,它在右边打开。这之后有两个动作。

  1. 计算。文章的总字符数被计算为(num_chars(title) + num_chars(text) ,并显示出来。
  2. 网络请求。根据文章的总字符数,通过网络请求获取一个表情符号并显示。随着字符数的增加,表情符号会从悲伤变为快乐。

构建应用程序

我们将通过四个步骤来了解构建这个应用程序的正确方法。

  • 不正确:该应用程序没有像预期的那样工作--当选择新的文章时,计算和网络请求都没有被触发。
  • 部分正确:该应用程序按预期工作,但在选择新文章时出现了闪烁效果。
  • 正确但次优。该应用程序按预期工作,没有DOM闪烁,但提出了不必要的网络请求。
  • 正确且最佳。该应用程序按预期工作,没有DOM闪烁和不必要的网络请求。

下面是这个应用程序的一个嵌入式沙盒。通过点击顶部导航栏中的相应选项来查看每种方法。检查当点击左侧导航菜单中的文章时,该应用程序的表现如何。

代码结构

你可以通过点击右下角的按钮来打开上述沙盒。

src/pages 目录中有映射到每个步骤的页面。src/pages 中每个页面的文件都包含一个ArticleContent 组件。正在讨论的代码就在这个ArticleContent 组件内。

现在让我们回顾一下上述四种方法中所遵循的反模式和模式。

反模式#1:道具或上下文作为初始状态

在不正确的方法中,propscontext 已被用作useStateuseRef 的初始值。在第27行,我们可以看到平均长度已被计算并存储为一个状态。

这个反模式是在选择新的文章时,计算和网络请求都没有被触发的原因。

反模式#2:销毁和重新创建

让我们通过使用 "销毁和重新创建 "的反模式来弥补我们不正确的方法。

销毁一个功能组件是指销毁所有的钩子和在第一次函数调用时创建的状态。重新创建是指再次调用该函数,就像之前从未被调用过一样。

请注意,一个父级组件可以使用key 道具来销毁该组件,并在每次key 变化时重新创建它。是的,你没看错--你可以在循环外使用键。

具体来说,我们通过在RecreateDoc.tsx 文件中渲染父组件RecreateDoc 的子组件ArticleContent (第89行)时使用key 道具来实现 "销毁和重新创建 "反模式。

该应用程序按预期工作,但当选择新的文章时,会出现闪烁的效果。因此,这个反模式的结果是部分正确的输出。

模式#1:JSX的内部状态

在这个 "正确但次优 "的方法中,我们将使用 "重新渲染",而不是使用 "销毁和重新创建 "反模式。

重新渲染指的是再次调用react功能组件,并在跨函数调用中保持钩子的完整。请注意,在'销毁和重新创建'中,所有的钩子首先被销毁,然后从头开始重新创建。

为了实现'重新渲染',useEffectuseState 将被串联起来使用。useState 的初始值可以设置为nullundefined ,一旦useEffect 运行后,就会计算出一个实际值并分配给它。在这个模式中,我们通过使用useEffect ,规避了useState 中缺乏依赖阵列的问题。

具体来说,请注意我们是如何将总字符数的计算移到JJSX中的(第50行),并且我们将props (第41行)作为useEffect (第31行)中的一个依赖项。

使用这种模式,闪烁的效果已经被避免了,但每当道具发生变化时,就会有一个网络请求来获取表情符号。因此,即使字符数没有变化,也会有一个不必要的请求来获取相同的表情符号。

模式#2:道具是useMemo中的一个依赖项

这一次,让我们正确地、最优化地做这件事。这一切都从反模式1开始:propscontext 作为初始状态。

我们可以通过将props 作为useMemo 的依赖关系来解决这个问题。通过将总字符数的计算转移到useMemo 钩子上,我们能够防止网络请求获取表情符号,除非平均长度发生了变化。

结论

在这篇文章中,我们讨论了使用propscontext 作为初始状态和'Destroy and Recreate'是反模式,而使用JSX的内部状态和props 作为useMemo 的依赖是好模式。我们还了解到,当我们使用没有依赖数组的钩子和嵌套来安排React组件时,我们应该谨慎行事。

Want to Connect?