React中创建递归组件

您好,我是沧沧凉凉,是一名前端开发者,目前在掘金知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。


这段时间我已经完全被React吸引了,在React中,一切皆有可能,从5月底正式在项目中使用React开始,到现在逐步开始看一些关于React底层的一些知识,随着对React的逐渐深入了解,越来越觉得React是那么美妙与神奇。

JSX是React的标志,JSX拥有JavaScript的完全编程能力,所以它也可以创造递归组件,递归组件就是自己调用自己的组件,用到递归组件的例子比如导航栏等等,一般是后端返回的数据层级不确定的情况下,我们要进行渲染,这个时候你就可以使用到递归组件,比如下面这个例子:

recursion

可以看出来,每一个按钮下的层级是不一样的,有的只有一层,有的有两层,有的有三层,这种层数不固定的组件,我们就需要使用到递归组件来进行处理。

其实在React的项目编写过程中,庞大的社区已经提供了非常多的UI组件供你选择,几乎已经能够满足你的大部分需求,但是不怕一万就怕万一,如果你遇到一个需求,刚好找不到你需要的这种组件,那么这时你就需要手写一个递归组件。

1. 数据格式

一般来讲,我们渲染这种递归组件时,数据一般都是来自后端,如果数据是写在前端的话,那就意味着数据不会发生改变,如果数据不会发生改变,那你写起来还不容易嘛,不用递归组件直接盒子堆叠也能够达成效果(当然导航栏的数据可能在前端)。

一般来自后端的数据格式为数组或者对象,往往对象里面有一个属性children,而这个属性又对应着一个数组,而这个数组里面又是一些对象,然后对象又包含着children属性,children属性下又是数组,数组又是一些对象,对象下面又有children属性…就相当于你搁这搁这搁这呢?这就组成了递归组件需要的数据。

那么上面图中使用到的数据为:

const testData = [
  {
    title: "0-0",
    key: "0-0",
    children: [
      {
        title: "0-0-0",
        key: "0-0-0",
        children: [
          { title: "0-0-0-0", key: "0-0-0-0" },
          { title: "0-0-0-1", key: "0-0-0-1" },
          { title: "0-0-0-2", key: "0-0-0-2" }
        ]
      },
      {
        title: "0-0-1",
        key: "0-0-1",
        children: [
          { title: "0-0-1-0", key: "0-0-1-0" },
          { title: "0-0-1-1", key: "0-0-1-1" },
          { title: "0-0-1-2", key: "0-0-1-2" }
        ]
      },
      {
        title: "0-0-2",
        key: "0-0-2"
      }
    ]
  },
  {
    title: "0-1",
    key: "0-1",
    children: [
      { title: "0-1-0-0", key: "0-1-0-0" },
      { title: "0-1-0-1", key: "0-1-0-1" },
      { title: "0-1-0-2", key: "0-1-0-2" }
    ]
  },
  {
    title: "0-2",
    key: "0-2"
  }
];

可以看到上面的数据中,如果没有children则表示只有一层,即点击后不会有数据进行展示。

而有多层数据的,点击第一层的数据,就会展示第二层的数据,继续点击就可以继续展示下一层的数据。

2. 创建组件

首先,我们要了解一些ES11中非常有用的新特性!可选链操作符:?:

const data = {
  name: "张三",
  age: 18,
  sex: "男"
};

console.log(data.friend.name); // 报错:Uncaught TypeError: Cannot read property 'name' of undefined

通常我们在进行上面调用的情况下,在Vue2.x中可能会出现白屏,而在React中会直接报错。

而这个时候我们就需要将console.log(data.friend.name);写成console.log(data.friend && data.friend.name);,如果层级很深或者需要判断的属性很多,你就需要写大量的判断代码。

而为了解决这个问题ES11就发布了一个新特性可选链操作符:?:

console.log(data.friend && data.friend.name);就等同于console.log(data.friend?.name);,是不是写起来非常简单?

值得一提的是,在Vue2.x中,如果你使用了最新的Babel那么你是可以在<script>标签或者js文件中使用这项特性,但是却无法在<template>标签中使用这项特性,而在Vue3.x中,是可以在<template>中进行使用。

我们写递归组件和写递归函数一样要分为两个步骤:

  1. 编写基线渲染条件。
  2. 编写递归渲染条件。

比如要写一个上面演示的组件,那么你就需要思考哪些部分是递归渲染出来的,很明显,除了第一层外,都应该是递归进行渲染出来的,那么我们的重心就应该放在第一层组件应该如何编写上面。

这个时候又需要思考一个问题,如何渲染第一层组件,很显然,通过map循环进行渲染。

理清了这个思路,我们就可以开始编写组件了:

<Space>
    {/* 渲染首层 */}
    {data?.map((item) => {
        return (
            <Tag.CheckableTag key={item.key}>{item.title}</Tag.CheckableTag>
     );
    })}
</Space>

历经了千辛万苦,首层终于渲染出来了,这个时候你应该可以看到下面的样子:

image-20210920172655321

然后你就该思考如何实现点击选中的效果,这时你需要一个state状态,来储存当前点击的是哪一项:

// 储存点击项的下标
const [select, setSelect] = useState(0);

<Space>
    {/* 渲染首层 */}
    {data?.map((item, index) => {
        return (
            <Tag.CheckableTag
            onClick={() => {
            // 将点击项的下标储存起来
            setSelect(index);
        }}
     checked={select === index}
     key={item.key}
     >
         {item.title}
     </Tag.CheckableTag>
     );
    })}
</Space>

这个时候你就能得到下面的效果:

check

好了,点击效果也有了,你也将当前被点击的值记录下来了,那么就该来到最后一步了,递归渲染!最后一步简单的超出你的想象。

{/* 判断选中的项是否有children,如果有则递归渲染 */}
{data?.[select]?.children ? (
    <Test data={data?.[select]?.children} />
) : null}

没错,就是这么一句代码,就实现了递归渲染,你就得到了本文开头的效果。

所有代码长这个样子:

import React, { useState } from "react";
import "antd/dist/antd.css";
import "./styles.css";
import { Space, Tag } from "antd";

const testData = [
  {
    title: "0-0",
    key: "0-0",
    children: [
      {
        title: "0-0-0",
        key: "0-0-0",
        children: [
          { title: "0-0-0-0", key: "0-0-0-0" },
          { title: "0-0-0-1", key: "0-0-0-1" },
          { title: "0-0-0-2", key: "0-0-0-2" }
        ]
      },
      {
        title: "0-0-1",
        key: "0-0-1",
        children: [
          { title: "0-0-1-0", key: "0-0-1-0" },
          { title: "0-0-1-1", key: "0-0-1-1" },
          { title: "0-0-1-2", key: "0-0-1-2" }
        ]
      },
      {
        title: "0-0-2",
        key: "0-0-2"
      }
    ]
  },
  {
    title: "0-1",
    key: "0-1",
    children: [
      { title: "0-1-0-0", key: "0-1-0-0" },
      { title: "0-1-0-1", key: "0-1-0-1" },
      { title: "0-1-0-2", key: "0-1-0-2" }
    ]
  },
  {
    title: "0-2",
    key: "0-2"
  }
];

function Test({ data }) {
  // 储存点击项的下标
  const [select, setSelect] = useState(0);

  return (
    <div>
      <Space>
        {/* 渲染首层 */}
        {data?.map((item, index) => {
          return (
            <Tag.CheckableTag
              onClick={() => {
                // 将点击项的下标储存起来
                setSelect(index);
              }}
              checked={select === index}
              key={item.key}
            >
              {item.title}
            </Tag.CheckableTag>
          );
        })}
      </Space>
      {/* 判断选中的项是否有children,如果有则递归渲染 */}
      {data?.[select]?.children ? (
        <Test data={data?.[select]?.children} />
      ) : null}
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <Test data={testData} />
    </div>
  );
}

到这里,我们仅仅用了20多行代码,就实现了一个看似复杂的组件。

3. 最后

递归组件在某些情况下非常实用,尤其是在你不知道要展示的数据究竟有多少层的情况下,所以递归组件也是一个进阶路上需要掌握的技能,掌握了这项技能,你才可以写出更加复杂的组件。