React中创建递归组件
您好,我是沧沧凉凉,是一名前端开发者,目前在掘金、知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。
这段时间我已经完全被React吸引了,在React中,一切皆有可能,从5月底正式在项目中使用React开始,到现在逐步开始看一些关于React底层的一些知识,随着对React的逐渐深入了解,越来越觉得React是那么美妙与神奇。
JSX是React的标志,JSX拥有JavaScript的完全编程能力,所以它也可以创造递归组件,递归组件就是自己调用自己的组件,用到递归组件的例子比如导航栏等等,一般是后端返回的数据层级不确定的情况下,我们要进行渲染,这个时候你就可以使用到递归组件,比如下面这个例子:
可以看出来,每一个按钮下的层级是不一样的,有的只有一层,有的有两层,有的有三层,这种层数不固定的组件,我们就需要使用到递归组件来进行处理。
其实在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>
中进行使用。
我们写递归组件和写递归函数一样要分为两个步骤:
- 编写基线渲染条件。
- 编写递归渲染条件。
比如要写一个上面演示的组件,那么你就需要思考哪些部分是递归渲染出来的,很明显,除了第一层外,都应该是递归进行渲染出来的,那么我们的重心就应该放在第一层组件应该如何编写上面。
这个时候又需要思考一个问题,如何渲染第一层组件,很显然,通过map
循环进行渲染。
理清了这个思路,我们就可以开始编写组件了:
<Space> {/* 渲染首层 */} {data?.map((item) => { return ( <Tag.CheckableTag key={item.key}>{item.title}</Tag.CheckableTag> ); })}</Space>
历经了千辛万苦,首层终于渲染出来了,这个时候你应该可以看到下面的样子:
然后你就该思考如何实现点击选中的效果,这时你需要一个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>
这个时候你就能得到下面的效果:
好了,点击效果也有了,你也将当前被点击的值记录下来了,那么就该来到最后一步了,递归渲染!最后一步简单的超出你的想象。
{/* 判断选中的项是否有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. 最后
递归组件在某些情况下非常实用,尤其是在你不知道要展示的数据究竟有多少层的情况下,所以递归组件也是一个进阶路上需要掌握的技能,掌握了这项技能,你才可以写出更加复杂的组件。