React学习笔记(番外二)——列表多选批量处理复合组件

news2024/11/28 9:45:40

React学习笔记(番外二)——列表多选批量操作复合组件

  • 前言
  • 〇、Show you the code
  • 一、 任务分析及拆解
    • 表头行的Checkbox——总开关
    • 记录行的Checkbox——行级开关
  • 二、 基础实现
    • 表头行的文件——header-row.js
    • 记录行的文件——record-row.js
    • 页面的文件App.js
    • 阶段效果
  • 三、 完善控制逻辑
    • custom-hooks.js——自定义React Hook
      • 4处useState
      • 4处useCallback
      • 1处useMemo
    • App.js——部分改造
    • header-row.js——部分改造
    • record-row.js——部分改造
    • 阶段效果
  • 四、 最后一步
  • 后记

前言

近期有需求需要实现如题的列表多选批量处理复合组件。因为是两部分组件互相控制,对于初次实现的我来说还是有一定困难的。经过几天的实现、Bugfix,现对这种比较常见的复合组件的实现作以记录。


〇、Show you the code

成熟的程序猿讲究【Talk is cheap, show me the code】,如果你只想看代码:

本篇文章中,最终的完整代码已托管在CodeSandBox,点击前往,查看代码及编译、渲染结果


一、 任务分析及拆解

先来看一下上面说的到底是什么。如下图,当我们有一个比较长的列表时,有时希望一次性多选几行,进行批量操作。
请添加图片描述

我们可以很轻松地将所需的复合组件拆分为以下几部分子组件:

  • 表头行的Checkbox——总开关
  • 记录行的Checkbox——行级开关
  • 右上方的操作按钮

表头行的Checkbox——总开关

这个Checkbox比较特殊,有3个状态,如下图:
Checkbox的3种状态

  1. 未选中状态: 点击后变为全部选中状态。此状态下,列表中的记录都是未选中状态。表头行Checkbox由于点击达到该状态时所有记录行Checkbox都变为未选中状态,所有记录行Checkbox由于点击都达到未选中状态时,表头行Checkbox也受控变为未选中状态。
  2. 部分选中状态: 点击后变为全部选中状态。此状态下,列表中有部分记录([1, n-1])为选中状态。该状态只能由部分记录行Checkbox受到点击变为选中状态而达到。
  3. 全部选中状态: 点击后变为未选中状态。此状态下,列表中的记录都是选中状态。表头行Checkbox由于点击达到该状态时所有记录行Checkbox都变为选中状态,所有记录行Checkbox由于点击都达到选中状态时,表头行Checkbox也受控变为全部选中状态。

记录行的Checkbox——行级开关

记录行前的就是常规的Checkbox,只有两种状态,如下图:
Checkbox的2种状态

  1. 未选中状态: 点击后变为选中状态。此状态下,当前行的记录是未选中状态。所有记录行Checkbox都为该状态时表头行Checkbox受控变为未选中状态。
  2. 选中状态: 点击后变为未选中状态。此状态下,当前行的记录是选中状态。部分记录行Checkbox为该状态时表头行Checkbox受控变为部分选中状态。所有记录行Checkbox都为该状态时表头行Checkbox受控变为全部选中状态。

二、 基础实现

我们将一个包含列表的页面分为三个部分:

  • 表头行的文件——header-row.js
  • 记录行的文件——record-row.js
  • 页面的文件——App.js

接下来分别实现三个部分

表头行的文件——header-row.js

import "./index.css";
import React from "react";
import { Checkbox } from "antd";

const HeaderRow = () => {
  return (
    <div className="header-row">
      <div className="column0">
        <Checkbox />
      </div>
      <div className="column1">Id</div>
      <div className="column2">Title</div>
      <div className="column3">Desc</div>
    </div>
  );
};

export default HeaderRow;

记录行的文件——record-row.js

import "./index.css";
import React from "react";
import { Checkbox } from "antd";

const RecordRow = (props) => {
  let { record } = props;
  let { id, title, desc } = record;
  return (
    <div className="record-row">
      <div className="column0">
        <Checkbox />
      </div>
      <div className="column1">{id}</div>
      <div className="column2">{title}</div>
      <div className="column3">{desc}</div>
    </div>
  );
};

export default RecordRow;

页面的文件App.js

import "./styles.css";
import React, { useMemo } from "react";
import HeaderRow from "./components/header-row";
import RecordRow from "./components/record-row";

const MainPage = (props) => {
  // 生成要显示的记录数据,现实中应该通过后台请求获得
  let records = useMemo(() => {
    let _records = [];
    for (let i = 0; i < 10; i++) {
      _records.push({
        id: i + 1,
        title: `这是测试标题${i + 1}`,
        desc: `这是测试描述${i + 1}`
      });
    }
    return _records;
  }, []);

  // 记录行的组件列表
  let recordRowViews = useMemo(() => {
    return records.map((item) => {
      return <RecordRow key={`record-${item.id}`} record={item} />;
    });
  }, [records]);

  return (
    <div>
      <HeaderRow />
      {recordRowViews}
    </div>
  );
};

export default MainPage;

阶段效果

基础实现比较简单,当然功能也不完善。我们还没有写Checkbox之间的控制逻辑,先看看目前的效果:
基础实现操作动图


三、 完善控制逻辑

因为实现中涉及到多个页面具有同样的控件和逻辑需要,因此实现中尽可能地考虑代码的复用性。这里的代码实现拆分为以下几个部分:

  • custom-hooks.js——包含几个自定义React Hook,最主要的Checkbox互相控制算法逻辑部分
  • App.js——前文中已有的文件,做部分改造
  • header-row.js——前文中已有的文件,做部分改造
  • record-row.js——前文中已有的文件,做部分改造

custom-hooks.js——自定义React Hook

import { useState, useMemo, useCallback } from "react";

/**
 * 列表多选,批量处理的逻辑Hook
 * @param showedRecords 经过过滤后页面需要显示的列表项
 * @returns {[[string],boolean,boolean,boolean,function,function]}
 */
const useBatchOperation = (allRecords) => {
  // 表示部分[1, n-1]行被勾选的状态,仅控制UI
  let [partialChecked, setPartialChecked] = useState(false);
  // 表示是否所有的条目当前都被选中(即UI表现为表头的checkbox是否勾选)
  let [headerChecked, setHeaderChecked] = useState(false);
  // 表示当前用户是否主动点击了表头的那一个checkbox,点击后无论状态如何,其他checkbox都要跟随该状态,同headerChecked配合控制使用
  let [operateAll, setOperateAll] = useState(false);
  // 当前勾选的行所代表的数据单元组成的列表
  let [checkedItemList, setCheckedItemList] = useState([]);

  // 选中某行记录
  let checkItem = useCallback(
    (item) => {
      let newCheckedItemList = [...checkedItemList];
      if (!Array.isArray(item)) {
        item = [item];
      }
      for (let i = 0; i < item.length; i++) {
        let index = checkedItemList.indexOf(item[i]);
        if (index === -1) {
          newCheckedItemList.push(item[i]);
        }
      }
      setCheckedItemList(newCheckedItemList);
      return newCheckedItemList;
    },
    [checkedItemList]
  );

  // 取消选中某行记录
  let uncheckItem = useCallback(
    (item, clear = false) => {
      let newCheckedItemList = [];
      if (!clear) {
        if (!Array.isArray(item)) {
          item = [item];
        }
        for (let i = 0; i < checkedItemList.length; i++) {
          if (item.indexOf(checkedItemList[i]) === -1) {
            newCheckedItemList.push(checkedItemList[i]);
          }
        }
      }
      setCheckedItemList(newCheckedItemList);
      return newCheckedItemList;
    },
    [checkedItemList]
  );

  // 记录行checkbox的onChange事件
  let onSingleCheckBoxChange = useCallback(
    (checked, record) => {
      // 按照判断逻辑,必须放在第一行
      setOperateAll(false);
      let newCheckedItemList;
      if (checked) {
        newCheckedItemList = checkItem(record);
      } else {
        newCheckedItemList = uncheckItem(record);
      }
      setPartialChecked(
        newCheckedItemList.length > 0 &&
          newCheckedItemList.length < allRecords.length
      );
      setHeaderChecked(newCheckedItemList.length === allRecords.length);
    },
    [checkItem, uncheckItem, allRecords]
  );

  // 表头行checkbox的onChange事件
  let onBatchCheckBoxChange = useCallback(() => {
    if (!headerChecked) {
      checkItem(allRecords);
    } else {
      uncheckItem(null, true);
    }
    setPartialChecked(false);
    setHeaderChecked(!headerChecked);
    setOperateAll(true);
  }, [headerChecked, checkItem, uncheckItem, allRecords]);

  // 选中的记录,其Id组成的列表
  let checkedIdList = useMemo(() => {
    if (checkedItemList.length > 0) {
      return checkedItemList.map((item) => {
        return item.id;
      });
    }
    return [];
  }, [checkedItemList]);

  return [
    checkedIdList,
    partialChecked,
    operateAll,
    headerChecked,
    onSingleCheckBoxChange,
    onBatchCheckBoxChange
  ];
};

export { useBatchOperation };

这里代码量有110行,还是比较多的,我们拆开来讲一下逻辑。一共用了3种React Hook,分别是useStateuseCallbackuseMemo。其中useState4处,useCallback4处,useMemo1处。

4处useState

  // 表示部分[1, n-1]行被勾选的状态,仅控制UI
  let [partialChecked, setPartialChecked] = useState(false);
  // 表示是否所有的条目当前都被选中(即UI表现为表头的checkbox是否勾选)
  let [headerChecked, setHeaderChecked] = useState(false);
  // 表示当前用户是否主动点击了表头的那一个checkbox,点击后无论状态如何,其他checkbox都要跟随该状态,同headerChecked配合控制使用
  let [operateAll, setOperateAll] = useState(false);
  // 当前勾选的行所代表的数据单元组成的列表
  let [checkedItemList, setCheckedItemList] = useState([]);
  • partialChecked: 标识表头行Checkbox当前是否处于部分选中状态,仅用于控制UI样式,初始值为false
  • headerChecked: 标识表头行Checkbox当前是否处于全部选中状态,即是否所有记录行Checkbox都为选中状态,初始值为false
  • operateAll: 标识当前用户是否主动点击了表头行Checkbox,点击后所有记录行Checkbox必须跟随改变为同样的状态,初始值为false
  • checkedItemList: 当前勾选的记录行所代表的数据组成的列表,前文段落页面的文件App.jsrecords数组的子集,初始值为空数组[]

4处useCallback

代码较长,不再复制徒增篇幅。

  • checkItem: 勾选某行记录行Checkbox后执行的事件方法。实际代码逻辑是给checkedItemList列表中新增一个记录数据,然后通过setCheckedItemList更新最新的checkedItemList,之所以使用useCallback封装一层,是为了其访问到的checkedItemList是当前最新的。
  • uncheckItem:checkItem作用相反,取消勾选某行记录行Checkbox后执行的事件方法。实际代码逻辑是在checkedItemList列表中找到并删除一个记录数据…
  • onSingleCheckBoxChange:记录行Checkbox被点击而发生状态变化时执行的事件方法。代码逻辑包括:根据当前状态是选中还是未选中调用checkItemuncheckItem更新checkedItemList,根据checkedItemListlength判断表头行Checkbox是否应该改为部分选中全部选中未选中状态等。
  • onBatchCheckBoxChange :表头行Checkbox被点击而发生状态变化时执行的事件方法。代码逻辑包括:根据当前状态是全部选中还是未选中调用checkItemuncheckItem更新checkedItemList等。

1处useMemo

  • checkedIdList: 根据checkedItemList实时计算,表示当前被选中的记录的id组成的列表,暴露出去,向后台发请求时用。

App.js——部分改造

......
const MainPage = (props) => {
  ......
  // 使用自定义hook-useBatchOperation,传入records列表
  let [
    checkedIdList,
    partialChecked,
    operateAll,
    headerChecked,
    onSingleCheckBoxChange,
    onBatchCheckBoxChange
  ] = useBatchOperation(records);

  let recordRowViews = useMemo(() => {
    return records.map((item) => {
      return (
        <RecordRow
          key={`record-${item.id}`}
          record={item}
          operateAll={operateAll}
          headerChecked={headerChecked}
          onSingleCheckBoxChange={onSingleCheckBoxChange}
        />
      );
    });
  }, [records, operateAll, headerChecked, onSingleCheckBoxChange]);

  return (
    <div>
      <HeaderRow
        partialChecked={partialChecked}
        headerChecked={headerChecked}
        onBatchCheckBoxChange={onBatchCheckBoxChange}
      />
      {recordRowViews}
    </div>
  );
};

export default MainPage;

为了不占过多篇幅,用......代替未发生改变的部分。

主要修改3个部分:

  1. 创建一个自定义Hook-useBatchOperation的实例,获得暴露出来的6个变量。
  2. RecrodRow传入operateAllheaderCheckedonSingleCheckBoxChange
  3. HeaderRow传入partialCheckedheaderCheckedonBatchCheckBoxChange

剩下暂时未用到的checkedIdList留给后面的batch-operation-buttons.js

header-row.js——部分改造

......
const HeaderRow = (props) => {
  let { partialChecked, headerChecked, onBatchCheckBoxChange } = props;
  ......
        <Checkbox
          indeterminate={partialChecked}
          checked={headerChecked}
          onChange={onBatchCheckBoxChange}
        />
      ......
  );
};

export default HeaderRow;

主要修改2个部分:

  1. 引入父组件App.js传入的partialCheckedheaderCheckedonBatchCheckBoxChange
  2. 将上述值和方法传入Checkbox控件

record-row.js——部分改造

......
const RecordRow = (props) => {
  ......
  let { record, operateAll, headerChecked, onSingleCheckBoxChange } = props;
  let [checked, setChecked] = useState(false);

  useEffect(() => {
    if (operateAll) {
      setChecked(headerChecked);
    }
  }, [operateAll, headerChecked, setChecked]);

  let wrappedOnChange = useCallback(() => {
    setChecked(!checked);
    onSingleCheckBoxChange(!checked, record);
  }, [checked, setChecked, record, onSingleCheckBoxChange]);

  return (
    ......
        <Checkbox
          checked={checked}
          onChange={wrappedOnChange}
          onClick={wrappedOnChange}
        />
      ......
  );
};

export default RecordRow;

主要修改4个部分:

  1. 引入父组件App.js传入的operateAllheaderCheckedonSingleCheckBoxChange
  2. 添加一个useEffect Hook,如果表头行Checkbox被点击,跟随其改变状态
  3. onSingleCheckBoxChange封装一层,被点击时不仅要更新自身状态,还要触发自定义Hook的数据更新
  4. 将上述值和方法传入Checkbox控件

阶段效果

  • 全选 / 取消全选
    请添加图片描述
  • 部分选中
    请添加图片描述

四、 最后一步

经过前文中的努力,UI上表现达到预期了,所需的数据(checkedIdList)也拿到了,距离实现文章开头的效果还差右上角的操作按钮。这里我们新建一个batch-operation-buttons.js

import "./index.css";
import React, { useCallback } from "react";

const BatchOperationButtons = (props) => {
  let { checkedIdList } = props;

  let handleClick = useCallback(
    (func) => {
      switch (func) {
        case 0:
          alert(`批量删除记录,IdList:${JSON.stringify(checkedIdList)}`);
          break;
        case 1:
          alert(`批量下载记录,IdList:${JSON.stringify(checkedIdList)}`);
          break;
        default:
          throw new TypeError(`操作码[${func}]未实现!`);
      }
    },
    [checkedIdList]
  );

  if (checkedIdList?.length < 1) {
    return null;
  }
  return (
    <div className="batch-operation-buttons">
      <span className="delete-button" onClick={() => handleClick(0)}>
        删除
      </span>
      <span>/</span>
      <span className="download-button" onClick={() => handleClick(1)}>
        下载
      </span>
    </div>
  );
};

export default BatchOperationButtons;

注意: 这里的alert是为了演示方便,现实情况下应该改为向后台发请求,按照checkedIdList和操作的func对选中的记录进行批量操作.

App.js中仅需改动以下代码即可

  ......
  return (
    <div>
      <div className="buttons">
        <BatchOperationButtons checkedIdList={checkedIdList} />
      </div>
      ......
    </div>
  );

后记

这次的实现中首次学习并使用了自定义Hook。实现之后发现了它的重要意义,即我们可以通过自定义Hook真正地将HTML模板和JS数据逻辑拆分为两个独立的文件,一个只管模板内容,一个只管数据的更新。官方一点的说法即:实现了Model(custom-hooks.js)View(App.js)的分离,意义重大。

为了便于理解,你可以想象,在学会使用自定义Hook之前,App.jscustom-hooks.js里的代码统统写在App.js里,这样一个控件里的代码量将非常大,维护起来比较麻烦。拆分为两个文件后,模板有Bug我们就只改View文件,数据有Bug我们就只改Model文件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/345144.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux操作系统学习(进程退出)

文章目录进程终止退出码进程退出场景进程退出的方式进程终止 退出码 ​ 我们知道每个main函数结尾都有return&#xff0c;一般都是return 0&#xff0c;用echo $&#xff1f;就可以查看它的退出码&#xff0c;0就是他的退出码&#xff0c;也可以写一些别的数值&#xff0c;比…

微服务项目【网关服务限流熔断降级分布式事务】

网关服务限流熔断降级 第1步&#xff1a;启动sentinel-dashboard控制台和Nacos注册中心服务 第2步&#xff1a;在网关服务中引入sentinel依赖 <!-- sentinel --> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-…

好好学react源码然后惊艳所有人

hello&#xff0c;这里是潇晨&#xff0c;今天我们来聊聊react源码&#xff0c;作为使用react多年的前端工程师&#xff0c;我们是否还在使用着各种应用层面的库、框架呢&#xff0c;是否在面试过程中遇到一些关于react源码方面的问题呢&#xff0c;如果是&#xff0c;那么是否…

数据结构与算法:java对象的比较

1.基本类型的比较 在Java中&#xff0c;基本类型的对象可以直接比较大小。 public class TestCompare {public static void main(String[] args) {int a 10;int b 20;System.out.println(a > b);System.out.println(a < b);System.out.println(a b);char c1 A;char…

互联网舆情监测公司监测哪些内容,TOOM北京舆情监测公司

互联网舆情监测公司是一种提供舆情监测、分析和管理服务的公司&#xff0c;其业务主要涉及互联网舆情监测、数据分析、报告撰写、危机处理等方面。这些公司通过使用各种技术和工具&#xff0c;帮助客户监测他们在互联网上的声誉和品牌形象&#xff0c;并提供相应的建议和解决方…

前端基于 Docker 的 SSR 持续开发集成环境实践

项目收益 整体开发效率提升20%。加快首屏渲染速度&#xff0c;减少白屏时间&#xff0c;弱网环境下页面打开速度提升40%。 权衡 在选择使用SSR之前&#xff0c;需要考虑以下事项&#xff01; SSR需要可以运行Node.js的服务器&#xff0c;学习成本相对较高。对于服务器而言&a…

“就业”or“创业”,大学生毕业如何选择,校园市场是新出路?

据公开数据显示&#xff0c;2023届全国高校毕业生预计达到1158万人&#xff0c;同比增加82万人。规模和增量创历史新高。今年就业形势再次复杂严峻起来。 “就业难”这个词在疫情下的毕业季尤为明显。超过1000万人同时加入了争夺就业机会的行列&#xff0c;形成了庞大的求职阵容…

网络安全-靶场搭建

网络安全-靶场搭建 靶场就是给你练习用的&#xff0c;因为如果你直接用其他网站搞会把他搞炸的&#xff08;犯法的&#xff09; 简单概括&#xff1a;把靶场文件放到Phpstudy的网站&#xff0c;放到根目录里面 要用到之前下载的phpstudy 然后开启mysql和apache&#xff0c;默…

【2023】Prometheus-Blackbox_exporter使用

目录1.下载及安装blackbox_exporter2.修改配置文件设置监控内容2.1.使用http方式作为探测3.与prometheus集成4.导入blackbox仪表盘进行观测1.下载及安装blackbox_exporter 下载安装包 wget https://github.com/prometheus/blackbox_exporter/releases/download/v0.23.0/black…

企业现代化管理模式中,数据指标体系是什么

当前时代&#xff0c;数据已经成为了构建现代社会的重要元素&#xff0c;渗透到人们生活的方方面面。在商业世界中&#xff0c;数据的传播应用更是按下了加速键&#xff0c;在短时间内就让各行各业的企业开始围绕数据进行信息化、数字化转型&#xff0c;把数据当作企业重要的战…

前端原生 CSS 跑马灯效果,无限轮播(横竖版本,带渐变遮罩,简单实用)

一、横版跑马灯 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-wid…

Redis入门到实战-数据结构 (四、原理篇)

一、动态字符串SDS 我们都知道Redis中保存的Key是字符串&#xff0c;value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。 不过Redis没有直接使用C语言中的字符串&#xff0c;因为C语言字符串存在很多问题&#xff1a; 获取字符串长度的需要通过…

计算机网络之http07 http2,http3

HTTP1.2 http1.2都做了哪些优化 (1)头部压缩 使用HPACK压缩头部 头部冗长&#xff0c;大量重复字段 &#xff08;2&#xff09;二进制帧 将报文头部和内容字符编码改为二进制格式 字符编码未压缩 &#xff08;3&#xff09;并发传输 解决h1.1 队头阻塞问题&#xff0c;多车道 …

opencv通道的分离与合并

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人的博客_CSDN博客-微信小程序,前端,python领域博主lqj_本人擅长微信小程序,前端,python,等方面的知识https://blog.csdn.net/lbcyllqj?spm1011.2415.3001.5343哔哩哔哩欢迎关注…

腾讯云 cos 字体在CDN上跨域处理

问题描述&#xff1a;项目中用到了字体的静态资源&#xff0c;把静态资源放到了腾讯云对象存储提供的 COS 上&#xff0c;同时启用它的CDN来加速。但是&#xff0c;调试的过程中发现报错&#xff1a;CSS加载字体跨域了&#xff0c;字体图标无法正常显示。 原因&#xff1a;字体…

torch.grid_sample

参考&#xff1a; 双线性插值的理论Pytorch grid_sample解析PyTorch中grid_sample的使用方法pytorch中的grid_sample()使用 查阅官方文档&#xff0c;TORCH.NN.FUNCTIONAL.GRID_SAMPLE grid_sample的函数签名如下所示&#xff0c;torch.nn.functional.grid_sample(input, gr…

Java设计模式总叙

文章目录基本概念设计模式的七大原则设计模式有哪些高频使用&经典的设计模式创建型-工厂方法模式创建型-建造者模式结构型-代理模式结构型-外观模式行为型-策略模式行为型-模板方法补充UML类图关系参考基本概念 使得代码编写真正工程化&#xff0c;它是软件工程的基石。 …

【cuda入门系列】通过代码真实打印线程ID

【cuda入门系列】通过代码真实打印线程ID1.gridDim(6,1),blockDim(4,1)2.gridDim(3,2),blockDim(2,2)【cuda入门系列之参加CUDA线上训练营】在Jetson nano本地跑 hello cuda&#xff01; 【cuda入门系列之参加CUDA线上训练营】一文认识cuda基本概念 【cuda入门系列之参加CUDA线…

ubuntu 20使用kubeadm安装k8s 1.26

步骤 机器&#xff1a;4核8G&#xff0c;root账号&#xff0c;可访问互联网 1、更新apt apt-get update 2、安装一些基本工具 apt-get install ca-certificates curl gnupg lsb-release net-tools apt-transport-https 3、ifconfig 获取ip&#xff0c;hostname获取主机名&…

【洛谷】P1195 口袋的天空

明显看出为最小生成树&#xff0c;那么&#xff1a;难点在哪里呢&#xff1f;if(cntn-k)//******{flag1;break;}为什么是cntn-k呢而不是k呢&#xff1f;&#xff01;&#xff01;&#xff01;解释&#xff1a;&#xff08;如果每个已经连在一起了就不能分开&#xff0c;不管多少…