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

理解 React 中的模式和反模式

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

介绍

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

  • 组件不会根据用户事件进行更新。
  • 组件根据用户事件部分更新。
  • 组件意外呈现。

我们的第一直觉当然是“在我们能找到的地方与邪恶作斗争”。

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

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


本文试图解释这些发现。

React 中的模式和反模式

  • 在本文中,如果符合以下条件,React 代码就可以称为良好的模式:
  • 该组件是可重复使用的。

代码更易于查看和调试。

请注意,如果我们编写更多代码行或者我们(预期)引入了一些额外的渲染以实现上述目标,则该代码仍被视为一种模式。


为什么即使是经验丰富的开发人员也会落入反模式的陷阱?

  1. React 代码在遵循模式与遵循反模式时看起来惊人地相似。
  2. 这种模式似乎很明显,以至于被忽略了。


如何识别反模式?

提示 #1:没有依赖数组的钩子

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

因此,在使用诸如 useState、useRef 等钩子时要小心,因为它们不使用依赖项数组。


提示 #2:在组合上嵌套

有两种机制用于排列 React 组件:

  1. 组成:所有孩子都有相同的数据
  2. 嵌套:每个孩子可以有不同的数据
我们如何减少 React 代码库中的错误

让我们想象一个场景,我们观察到“孩子 3”中有一个错误。

如果我们使用组合来排列组件,我们就不必查看“孩子 1”和“孩子 2”的代码,因为它们都是独立的。因此,调试的时间复杂度为 O (1)。

但是,如果我们使用嵌套来排列组件,我们将不得不检查“孩子 3”之前的所有孩子,以找出错误的来源。在这种情况下,调试的时间复杂度为 O (n),其中 n 是“孩子 3”以上的孩子的数量。

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


示例应用

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


应用程序的期望行为

在左侧导航菜单中单击文章时,它会在右侧打开。接下来是两个动作:

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


构建应用程序

我们将分四步了解构建此应用程序的正确方法:

  • 不正确:应用程序未按预期工作 - 选择新文章时不会触发计算或网络请求。
  • 部分正确:应用程序按预期工作,但在选择新文章时观察到闪烁效果。
  • 正确但次优:应用程序按预期运行,没有 DOM 闪烁,但会发出不必要的网络请求。
  • 正确且最佳:应用程序按预期工作,没有 DOM 闪烁和不必要的网络请求。

下面是这个应用程序的嵌入式沙盒。通过单击顶部导航栏中的相应选项来查看每种方法。检查在左侧导航菜单中单击文章时应用程序的执行情况。

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

代码结构

您可以通过单击右下角的按钮打开上述沙箱。

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


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


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

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

import { useCallback, useEffect, useState } from "react";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";
import { Navigation } from "../components/Navigation";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#FEE2E2",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 1. calculate length as we need it to get corresponding emotion
  const [length] = useState(
    props.article.text.length + props.article.title.length
  );

  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3. set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState("");
  useEffect(() => {
    if (emotions) {
      setEmotion(emotions["stickers"][length]);
    }
  }, [emotions, length]);

  return (
    
      
        

{props.article.title}

{props.article.text}

); }; const Incorrect: React.FC = () => { const articles = useGetArticles(); const [currentArticle, setCurrentArticle] = useState< Articles["articles"]["0"] | null >(); const onClickHandler = useCallback((article) => { setCurrentArticle(article); }, []); return ( {currentArticle ? : null} ); }; export default Incorrect;

这种反模式是选择新文章时既不触发计算也不触发网络请求的原因。


反模式 #2:销毁和重建

让我们通过使用“Destroy and Recreate”反模式来修正我们的错误方法。

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

请注意,父组件可以使用 key prop 来销毁组件并在每次 key 更改时重新创建它。 是的,你没看错——你可以在循环外使用键。

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

import { useCallback, useEffect, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#FEFCE8",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3, set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState("");
  useEffect(() => {
    if (emotions) {
      setEmotion(
        emotions["stickers"][
          props.article.text.length + props.article.title.length
        ]
      );
    }
  }, [emotions, props]);

  return (
    
      
        

{props.article.title}

{props.article.text}

); }; const PartiallyCorrect: React.FC = () => { const articles = useGetArticles(); const [currentArticle, setCurrentArticle] = useState< Articles["articles"]["0"] | null >(); const onClickHandler = useCallback((article) => { setCurrentArticle(article); }, []); return ( {currentArticle ? : null} ); }; export default PartiallyCorrect;

该应用程序按预期工作,但在选择新文章时会观察到闪烁效果。因此,这种反模式会导致部分正确的输出。


模式 #1:JSX 中的内部状态

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

重新渲染是指在函数调用之间保持完整的钩子再次调用 react 函数组件。请注意,在“Destroy and Recreate”中,首先销毁所有挂钩,然后从头开始重新创建。

为了实现“重新渲染”,useEffect 和 useState 将被串联使用。 useState 的初始值可以设置为 null 或 undefined,一旦 useEffect 运行,就会计算并分配一个实际值。在这个模式中,我们通过使用 useEffect 来规避 useState 中缺少依赖数组的问题。

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

import { useCallback, useEffect, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#FEF2F2",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 1. calculate length as we need it to get corresponding emotion
  const [length] = useState(
    props.article.text.length + props.article.title.length
  );

  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3. set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState("");
  useEffect(() => {
    if (emotions) {
      setEmotion(emotions["stickers"][length]);
    }
  }, [emotions, length]);

  return (
    
      
        

{props.article.title}

{props.article.text}

); }; const Suboptimal: React.FC = () => { const articles = useGetArticles(); const [currentArticle, setCurrentArticle] = useState< Articles["articles"]["0"] | null >(); const onClickHandler = useCallback((article) => { setCurrentArticle(article); }, []); return ( {/** Step 4. Using key to force destroy and recreate */} {currentArticle ? ( ) : null} ); }; export default Suboptimal;

使用这种模式,可以避免闪烁效果,但只要道具发生变化,就会发出网络请求来获取表情符号。 因此,即使字符数没有变化,也会发出不必要的请求来获取相同的表情符号。


模式 #2:props 作为 useMemo 中的依赖项

这次让我们以最佳方式正确地进行操作。 这一切都始于反模式#1:道具或上下文作为初始状态。

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

import { useCallback, useEffect, useMemo, useState } from "react";
import { Navigation } from "../components/Navigation";
import { useGetArticles } from "../hooks/useGetArticles";
import { useGetEmoji } from "../hooks/useGetEmoji";
import { Articles } from "../types";

const styles: { [key: string]: React.CSSProperties } = {
  container: {
    background: "#F0FDF4",
    height: "100%",
    display: "grid",
    gridTemplateColumns: "10rem auto"
  },
  content: {}
};

const ArticleContent: React.FC<{
  article: Articles["articles"]["0"];
}> = (props) => {
  // Step 1. calculate length as we need it to get corresponding emotion
  const length = useMemo(
    () => props.article.text.length + props.article.title.length,
    [props]
  );

  // Step 2. fetch emotion map from backend
  const emotions = useGetEmoji();

  // Step 3. set emotion once we get emotion map from backend
  const [emotion, setEmotion] = useState("");
  useEffect(() => {
    if (emotions) {
      setEmotion(emotions["stickers"][length]);
    }
  }, [emotions, length]);

  return (
    
      
        

{props.article.title}

{props.article.text}

); }; const Optimal: React.FC = () => { const articles = useGetArticles(); const [currentArticle, setCurrentArticle] = useState< Articles["articles"]["0"] | null >(); const onClickHandler = useCallback((article) => { setCurrentArticle(article); }, []); return ( {currentArticle ? : null} ); }; export default Optimal;


结论

在本文中,我们讨论了使用 props 或 context 作为初始状态和“Destroy and Recreate”是反模式,而在 JSX 中使用内部状态和 props 作为 useMemo 中的依赖项是很好的模式。 我们还了解到,当我们使用没有依赖数组和嵌套来排列 React 组件的钩子时,我们应该小心。


更多APP/小程序/网站源码资源,请搜索"七爪网源码交易平台"!

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章