跳转到主要内容

你在React中使用过递归组件吗?我有。我与他们的第一次接触让我对从事前端项目有了全新的认识。因此,我认为写一篇关于在React中使用递归组件的真实世界示例的文章是一个好主意,以帮助人们更熟悉使用它们。

React中的递归组件:一个真实世界的例子

在本文中,我们将探讨React中递归组件的细节及其在现实应用程序中的使用。我们将在React中查看递归和非递归组件的示例,并评估它们的模块性、可读性和可维护性。之后,我们将构建一个真实世界的递归组件,一个类似于VS Code的嵌套文件资源管理器。

向前跳:

  • React中的递归组件是什么?
  • 递归与React中的循环有何不同?
  • 为什么以及何时在React中使用递归组件
  • 使用标准React组件构建嵌套的文件资源管理器
  • 使用React中的递归组件构建嵌套的文件资源管理器

React中的递归组件是什么?

递归是在函数内部重复调用函数,直到满足基本条件(基本情况)。每次递归函数调用自己时,它都会接受一个新的参数。例如,下面的阶乘函数通过使用不同的参数重复调用自己来工作,直到满足基本条件:

function factorial(num) {
    if (num <= 1) { // base condition
        return 1;
    }
    return num * factorial(num - 1); // function calling itself with new input value.
}
console.log(factorial(6)); // 720 

现在我们已经为理解递归组件奠定了基础,让我们在React中看看它们。正如您所知,React组件是函数。当React组件使用不同的道具重复渲染其内部,直到满足基本条件时,它被称为递归组件。

递归组件是唯一的,因为它可以调用自己,并有助于呈现深度嵌套的数据。这些组件并不局限于简单地在UI上呈现父数据;它们还渲染父母子女的数据,直到达到深度限制。在这些组件中,可以通过传递道具来设置儿童。

让我们看看React中递归组件的一个简单示例:

import React from "react";

const RecursiveComponent = ({children}) => {
  return (
    <div>
      {children.length > 0 && <RecursiveComponent children={children} />}
    </div>
  );
};

export default RecursiveComponent;

如果你对这个概念在现实世界中的应用感到好奇,你可以在Reddit和类似于VS Code的文件浏览器中看到它们作为嵌套评论系统的作用。

现在您已经对递归组件有了基本的了解,我们将探索它们的用途,并构建一个真实的示例。

递归与React中的循环有何不同?

您可能想知道循环和递归之间的关键区别。在本节中,我们将了解这些差异。与循环相反,递归包括调用一个调用自身的函数,而循环则需要连续调用相同的代码,直到满足特定条件。

对于循环,我们必须首先定义我们的控制变量,然后才能在循环的任何迭代中使用它。例如,当我们创建循环以循环通过数组时,我们必须声明一个计数器变量,让i=0,以跟踪迭代次数。这允许我们在循环遍历数组时使用计数器变量。否则,我们将无法在需要时使用它,我们需要增加或减少这些控制变量以避免无限循环。

另一方面,在使用递归时,我们不必声明任何变量来执行递归操作。这是因为递归操作不依赖于变量,只需要一个基本条件就可以停止调用函数。

  • 可返回性:对于循环,我们不能返回任何内容。然而,当我们使用递归时,我们可以从函数返回一个值
  • 可读性和模块化:通过递归,代码变得更加可读和模块化。但是,使用循环会使代码变长

为什么以及何时在React中使用递归组件

在React中,组件是构建用户界面的主要构建块。它们非常棒,因为它们帮助我们全面思考我们的应用程序,并从更容易推理的较小代码块构建它。

你可能会想,“我们为什么要制作递归组件?”在React中使用递归组件的主要原因是它们使代码变得干燥、可读性更强、模块化。

解决了这个问题,让我们集中精力了解何时在React中使用递归组件。最常见的情况是当我们将数据嵌套到几个级别时。

假设我们有一个对象数组,每个对象都有一个子对象​对应于另一个对象数组的键。该数组的每个对象部分都有一个子键,对应于另一个对象数组。同样,这些对象也可以包含更多的子数组。

此类数据的示例如下:

export const data = [
  {
    isFolder: true,
    name: "public",
    children: [
      {
        isFolder: false,
        name: "index.html",
      },
    ],
  },
  {
    isFolder: true,
    name: "src",
    children: [
      {
        isFolder: true,
        name: "components",
        children: [
          {
            isFolder: true,
            name: "home",
            children: [
              {
                isFolder: false,
                name: "Home.js",
              },
            ],
          },
        ],
      },
      {
        isFolder: false,
        name: "App.js",
      },
    ],
  },
];

正如您所看到的,上面的数据有很多嵌套的子数组。这些嵌套项目可以更深入,但我只将其深入到四个层次,以清楚地解释事情。

在这种情况下使用循环不是一个好的选择,因为它需要编写大量嵌套循环来循环遍历每一级数据。这会使代码变得更大、更难阅读。此外,如果我们不确定数据的深度,那么使用循环遍历所有嵌套数据是很有挑战性的。因此,在这种情况下,最好使用递归。

使用标准React组件构建嵌套的文件资源管理器

在本节中,我们将使用标准React组件构建一个嵌套的文件资源管理器应用程序。在本教程的下一节中,我们将使用递归组件构建相同的React应用程序。

首先,在src文件夹中创建一个新的React应用程序和一个数据文件夹。然后,在数据中创建data.js,并将“为什么以及何时在React中使用递归组件”部分的数据复制并粘贴到后面的部分。

现在,将App.js文件的代码替换为以下代码:

import React from "react";
import { data } from "./data/data";

const NonRecursiveComponent = ({ data }) => {
  return <div>Non Recursive Component</div>;
};

const App = () => {
  return (
    <div style={{ margin: "8px" }}>
      <NonRecursiveComponent data={data} />
    </div>
  );
};
export default App;

我们在上面的代码中创建了一个NonRecursiveComponent,并在应用程序组件中进行了渲染。然后,我们将数据道具从应用程序传递给NonRecursiveComponent。

现在,让我们开始在UI上渲染数据。在NonRecursiveComponent中,将现有代码替换为以下代码:

const NonRecursiveComponent = ({ data }) => {
  return (
    <div>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            <span>{parent.name}</span>
          </div>
        );
      })}
    </div>
  );
};

上面的代码将呈现所有父级(一级)数据,并在UI上显示这些数据:

React中的渲染递归组件

现在,让我们通过在第二个return语句中使用map方法来渲染子级。您的代码应该如下所示:

const NonRecursiveComponent = ({ data }) => {
  return (
    <div>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            <span>{parent.name}</span>
            {parent.children.map((child) => { // rendering children of the parent
              return (
                <div key={child.name} style={{ paddingLeft: "20px" }}>
                  <span>{child.name}</span>
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
};

从那里,您应该可以在UI上看到父级的所有子级:

React递归组件UI中的子级

让我们把孙子孙女们交出来。因此,要做到这一点,我们需要做与我们为儿童所做的相同的事情。您的代码应该如下所示:

const NonRecursiveComponent = ({ data }) => {
  return (
    <div>
      {data.map((parent) => { // rendering parent data
        return (
          <div key={parent.name}>
            <span>{parent.name}</span>
            {parent.children.map((child) => { // rendering children of the parent
              return (
                <div key={child.name} style={{ paddingLeft: "20px" }}>
                  <span>{child.name}</span>
                  {child.children && // rendering grandchildren of the parent
                    child.children.map((grandChild) => {
                      return ( 
                        <div key={grandChild.name} style={{ paddingLeft: "20px" }}>
                          <span>{grandChild.name}</span>
                        </div>
                      );
                    })}
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
};

您应该在UI上看到这样的内容:

React中递归组件中的孙子女

现在,我们最不需要做的就是在UI上渲染曾孙。我们需要做与我们为子孙后代所做的相同的事情。完成此操作后,您的代码应该如下所示:

const NonRecursiveComponent = ({ data }) => {
  return (
    <div>
      {data.map((parent) => { // rendering parent data
        return (
          <div key={parent.name}>
            <span>{parent.name}</span>
            {parent.children.map((child) => { // rendering children of the parent
              return (
                <div key={child.name} style={{ paddingLeft: "20px" }}>
                  <span>{child.name}</span>
                  {child.children && // rendering grandchildren of the parent
                    child.children.map((grandChild) => {
                      return (
                        <div
                          key={grandChild.name}
                          style={{ paddingLeft: "20px" }}
                        >
                          <span>{grandChild.name}</span>
                          {grandChild.children && // rendering great-grandchildren
                            grandChild.children.map((greatGrandChild) => {
                              return (
                                <div
                                  key={greatGrandChild.name}
                                  style={{ paddingLeft: "20px" }}
                                >
                                  <span>{greatGrandChild.name}</span>
                                </div>
                              );
                            })}
                        </div>
                      );
                    })}
                </div>
              );
            })}
          </div>
        );
      })}
    </div>
  );
};

您的UI应该如下所示:

React递归组件文件

到目前为止,我们已经使用常规React组件构建了一个基本的嵌套文件资源管理器组件。但是,正如您所看到的,对于每个嵌套级别的数据,NonRecursiveComponent代码似乎都在重复自己。随着数据嵌套级别的提高,这些代码变得越来越长,使其更难理解和维护。

那么,我们该如何解决这个问题呢?在下一节中,我们将研究React中的递归组件如何提供帮助。

使用React中的递归组件构建嵌套的文件资源管理器

在本节中,我们将使用递归组件构建相同的嵌套文件资源管理器,并为嵌套文件和文件夹实现显示/隐藏功能。

让我们首先在App.js文件中创建一个新的RecursiveComponent。然后,将应用程序中呈现的NonRecursiveComponent替换为RecursiveComponent。

您的代码应该如下所示:

const RecursiveComponent = ({ data }) => {
  return (
    <div style={{ paddingLeft: "20px" }}>
      Recursive Component
    </div>
  );
};

const App = () => {
  return (
    <div style={{ margin: "8px" }}>
      <RecursiveComponent data={data} />
    </div>
  );
};

现在,让我们在UI上渲染所有父数据:

const RecursiveComponent = ({ data }) => {
  return (
    <div style={{ paddingLeft: "20px" }}>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            <span>{parent.name}</span>
          </div>
        );
      })}
    </div>
  );
};

接下来,让我们使用递归来渲染所有嵌套的文件和文件夹数据。要做到这一点,我们需要做两件事:首先,使用不同的数据道具从递归组件内部渲染递归组件。其次,我们需要一个基本条件来停止渲染递归组件。

在我们的例子中,基本条件是当子级的长度为零时,或者当它们不存在时,此时我们不会调用RecursiveComponent。

您的代码应该如下所示:

const RecursiveComponent = ({ data }) => {
  return (
    <div style={{ paddingLeft: "20px" }}>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            <span>{parent.name}</span>
            {/* Base Condition and Rendering recursive component from inside itself */}
            <div>
              {parent.children && <RecursiveComponent data={parent.children} />}
            </div>
          </div>
        );
      })}
    </div>
  );
};

您的UI应该如下所示:

React递归组件的真实世界示例

😮惊喜通过几行代码,我们实现了相同的输出。这就是React中递归的魔力。

现在,让我们实现对嵌套文件和文件夹的显示/隐藏。在开始这样做之前,我们需要对代码进行一些更改。首先,我们需要有条件地使用数据中的isFolder键,将button标记用于文件夹的名称,将span标记用于文件的名称。我们这样做是因为我们只会将onClick事件添加到文件夹中,而不会添加到文件中,以显示或隐藏特定文件夹中的所有文件和文件夹。

更新后的代码应该如下所示:

const RecursiveComponent = ({ data }) => {
  return (
    <div style={{ paddingLeft: "20px" }}>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            {/* rendering folders */}
            {parent.isFolder && <button>{parent.name}</button>}
            {/* rendering files */}
            {!parent.isFolder && <span>{parent.name}</span>}
            <div>
              {parent.children && <RecursiveComponent data={parent.children} />}
            </div>
          </div>
        );
      })}
    </div>
  );
};

现在,让我们通过在RecursiveComponent中使用useState Hook创建一个状态来实现show/hide,如下所示:

import { useState } from "react";

const RecursiveComponent = ({ data }) => {
  const [showNested, setShowNested] = useState({});
  // ...
  // rest of the code
  // ...
};

此showNested状态变量将存储所有打开的文件夹,如下所示:

{
  public: true, // if the folder is opened, set it equal to true
  src: false // if the folder is not opened, set it equal to false
}

现在,创建一个函数来处理显示/隐藏:

const递归组件=({data})=>{

const[showNested,setShowNested]=useState({});

//处理显示/隐藏功能

const RecursiveComponent = ({ data }) => {
  const [showNested, setShowNested] = useState({});

  // handle show/hide functionality
  const toggleNested = (name) => {
    setShowNested({ ...showNested, [name]: !showNested[name] });
  };
  // ...
  // rest of the code
  // ...
};

toggleNested函数接受文件夹的名称作为参数,然后使用setShowNested功能更新showNested中该文件夹的值。这会将其从false更改为true,反之亦然。

现在,让我们向按钮添加一个onClick事件来调用toggleNested。然后,当用户单击任何文件夹时,将文件夹的名称作为参数传递,如下所示:

const RecursiveComponent = ({ data }) => {
  // ...
  // Rest of the code
  return (
    <div style={{ paddingLeft: "20px" }}>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            {parent.isFolder && (
              <button onClick={() => toggleNested(parent.name)}>
                {parent.name}
              </button>
            )}
            // ...
            // Rest of the code
            // ...
          </div>
        );
      })}
    </div>
  );

最后一步是更新display CSS属性,我们将添加到包装递归组件内部的递归组件的div中。当用户选择文件夹时,我们将通过添加文件夹名称作为该对象的键来更新showNested对象。我们还将其值设置为true,并添加一个检查以查看showNested中是否存在文件夹名称。

然后,我们将显示器设置为块。否则,我们将设置为none以隐藏嵌套的文件和文件夹:

const RecursiveComponent = ({ data }) => {
  // ...
  // Rest of the code
  return (
    <div style={{ paddingLeft: "20px" }}>
      {data.map((parent) => {
        return (
          <div key={parent.name}>
            // ...
            // Rest of the code
            // ...

            // Updating the display property using the showNested state
            <div style={{ display: !showNested[parent.name] && "none" }}>
              {parent.children && <RecursiveComponent data={parent.children} />}
            </div>
          </div>
        );
      })}
    </div>
  );
};

希望您能够切换所有目录。说完,这个博客就结束了!

结论

在本文中,您已经了解了React中的递归组件,如何构建它们,以及我们可能使用它们的原因。此外,我还用一个真实世界的例子展示了递归与循环的区别。希望你学到了一些对你的下一个React项目有用的新东西。

我希望你喜欢这篇文章,感谢你花时间阅读。如果你在阅读这篇文章时有任何问题或有进一步的问题,请在评论区告诉我。如果你喜欢我在这里做的事情,并想帮助我继续做下去,别忘了点击分享按钮。

几分钟内即可使用LogRocket的现代React错误跟踪:

参观 https://logrocket.com/signup/获取应用ID

通过npm或脚本标记安装LogRocket。LogRocket.int()必须调用客户端,而不是服务器端

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');

(可选)安装插件,以便与您的堆栈进行更深层次的集成:

  • Redux middleware
  • NgRx middleware
  • Vuex plugin