React Hooks之封装表格组件

前言

我在做后台管理系统的时候发现,我们的系统有大量的表格查询模块,几乎每个页面都有一个表格查询功能,所以我利用react hooks的特性以及查阅网上的文章,封装了一个适合业务的通用hooks表格组件,供大家参考。

组件目录结构

image.png

index.tsx 就是我们的组件主文件,index.less 用于自定义表格样式, type.ts 用于定义接口类型

组件实现

1. 组件的功能

  • 支持自定义表头配置
  • 支持自定义查询方法
  • 查询时自动loading
  • 支持翻页功能
  • 支持配置是否初始化

2. 组件 Props

首先我们看一下组件接收哪些参数

也就是我们定义的接口 ArgTableProps

baseProps 用于接收Table组件的自带属性

owncolumns 是一个函数,用来生成 table组件需要的表头配置

queryAction 查询操作方法,返回一个异步查询方法

params 表格的查询参数

defaultCurrent 表格默认页码

noInit 初始化不直接查询操作

// types.ts
import { TableProps } from 'antd/lib/table/Table';

export interface Columns {
  [propName: string]: any;
}

// 查询方法
interface queryActionType {
  (arg: any): Promise<any>;
}

// 生成表头方法
export interface ColumnFunc {
  (updateMethod?: queryActionType): Array<Columns>;
}

// 表格组件Props
export interface ArgTableProps {
  baseProps?: TableProps<any>;
  owncolumns: ColumnFunc;
  queryAction: queryActionType;
  params?: any;
  defaultCurrent?: number;
  noInit?: boolean;
}

// 页码状态
export interface paginationInitialType {
  current: number;
  pageSize: number;
  total: number;
}

// 表格初始化状态
export interface initialStateType {
  loading: boolean;
  pagination: paginationInitialType;
  dataSource: Array<any>;
}

// 操作类型
export interface actionType {
  type: string;
  payload?: any;
}

3. 使用useReducer集中管理组件状态

// 分页数据
  const paginationInitial: paginationInitialType = {
    current: defaultCurrent || 1,
    pageSize: 10,
    total: 0,
  };

    // table组件全量数据
  const initialState: initialStateType = {
    loading: false,                   // 加载状态
    pagination: paginationInitial,    // 分页状态
    dataSource: [],                   // 表格数据
  };

  const reducer = useCallback((state: initialStateType, action: actionType) => {
    const { payload } = action;
    switch (action.type) {
      case 'TOGGLE_LOADING':
        return { ...state, loading: !state.loading };
      case 'SET_PAGINATION':
        return { ...state, pagination: payload.pagination };
      case 'SET_DATA_SOURCE':
        return { ...state, dataSource: payload.dataSource };
      default:
        return state;
    }
  }, []);

const [state, dispatch] = useReducer(reducer, initialState);

4. 表格的核心查询方法

我们封装的表格一个最主要的功能就是我们只需要传入一个定义好的接口方法,表格组件就可以自动查询数据,查询方法如下

async function fetchData(currentPage?: number | undefined) {
    dispatch({
      type: 'TOGGLE_LOADING',
    });
  
    // 分页字段名称转换
    const { current: pageNum, pageSize } = state.pagination;
    const concatParams = { pageNum: currentPage || pageNum, pageSize, ...params };
    const res = await queryAction(concatParams).catch(err => {
      console.log('请求表格数据出错了');
      dispatch({ type: 'TOGGLE_LOADING' });
      return { err };
    });
  
    // 关闭loading
    dispatch({
      type: 'TOGGLE_LOADING',
    });
  
    if (res && res.code && res.code === 1) {     // 这里根据具体后台接口返回判断
      const { body } = res;
      const list = body && body.records ? body.records : body;

      // 根据后端接口返回设置总页码数和数据总数
      dispatch({
        type: 'SET_PAGINATION',
        payload: {
          pagination: {
            ...state.pagination,
            current: currentPage || pageNum,
            total: (body && body.total) || 0,
          },
        },
      });
      
      // 回填list数据
      dispatch({
        type: 'SET_DATA_SOURCE',
        payload: {
          dataSource: list,
        },
      });
    }
  }

5. 组件结构

然后我们分析下组件结构,这里我们依赖 antdtable 组件和 paination 组件

这里 table 组件本身也有自带的页码组件,但是我们为了更好调整表格和页码布局自己引入了页码组件

return (
    <Fragment>
      <Table
        className={styles.table_wrap}
        columns={owncolumns(fetchData)}
        rowKey={(record: any) => record.id}
        pagination={false}
        dataSource={state.dataSource}
        loading={state.loading}
        {...baseProps}
      />
      <Row style={{ marginTop: 20 }} justify="end">
        <Col>
          <Pagination
            defaultCurrent={1}
            current={state.pagination.current}
            total={state.pagination.total}
            showTotal={total => `总共 ${total} 条数据`}
            showSizeChanger
            onChange={handleTableChange}
          />
        </Col>
      </Row>
    </Fragment>
  );

6. 执行逻辑

我们使用 useCallback 得到两个函数 fetchDataWarp 和 paramsChangeFetch

fetchDataWarp 作用是翻页状态变化时调用查询方法

paramsChangeFetch 作用是查询条件参数变化时调用查询方法

const [isInit, setIsInit] = useState(true);       // 是否是第一次加载数据
  const [mount, setMount] = useState(false);        // 表格组件是否已挂载

// useCallback包装请求,缓存依赖,优化组件性能
  const fetchDataWarp = useCallback(fetchData, [
    state.pagination.current,
    state.pagination.pageSize,
  ]);

  const paramsChangeFetch = useCallback(fetchData, [params]);

  useEffect(() => {
    if (!(noInit && isInit)) {          // 如果是第一次加载且noInit参数为true则不执行查询方法
      fetchDataWarp();
    }
    if (isInit) {           
      setIsInit(false);
    }
  }, [fetchDataWarp]);

  // 查询参数变化重置页码
  useEffect(() => {
    if (mount) {             // 只在第一次挂载后参数变化才执行查询方法
      paramsChangeFetch(1);
    }
    setMount(true);
  }, [paramsChangeFetch]);

7. 全部代码

// index.tsx
import { Table, Pagination, Row, Col } from 'antd';
import React, { useEffect, useReducer, useCallback, Fragment, useState } from 'react';
import { ArgTableProps, paginationInitialType, initialStateType, actionType } from './type';
import styles from './index.less';

const useAsyncTable: React.FC<ArgTableProps> = props => {
  const { owncolumns, queryAction, params, baseProps, defaultCurrent, noInit = false } = props;
  // 分页数据
  const paginationInitial: paginationInitialType = {
    current: defaultCurrent || 1,
    pageSize: 10,
    total: 0,
  };
  // table组件全量数据
  const initialState: initialStateType = {
    loading: false,
    pagination: paginationInitial,
    dataSource: [],
  };

  const reducer = useCallback((state: initialStateType, action: actionType) => {
    const { payload } = action;
    switch (action.type) {
      case 'TOGGLE_LOADING':
        return { ...state, loading: !state.loading };
      case 'SET_PAGINATION':
        console.log('reducer更新', payload);
        return { ...state, pagination: payload.pagination };
      case 'SET_DATA_SOURCE':
        return { ...state, dataSource: payload.dataSource };
      default:
        return state;
    }
  }, []);

  const [state, dispatch] = useReducer(reducer, initialState);
  const [isInit, setIsInit] = useState(true);
  const [mount, setMount] = useState(false);

  async function fetchData(currentPage?: number | undefined) {
    dispatch({
      type: 'TOGGLE_LOADING',
    });
    // 分页字段名称转换
    const { current: pageNum, pageSize } = state.pagination;
    const concatParams = { pageNum: currentPage || pageNum, pageSize, ...params };
    const res = await queryAction(concatParams).catch(err => {
      console.log('请求表格数据出错了');
      dispatch({ type: 'TOGGLE_LOADING' });
      return { err };
    });
    // 关闭loading
    dispatch({
      type: 'TOGGLE_LOADING',
    });
    if (res && res.code && res.code === 1) {
      const { body } = res;
      const list = body && body.records ? body.records : body;

      dispatch({
        type: 'SET_PAGINATION',
        payload: {
          pagination: {
            ...state.pagination,
            current: currentPage || pageNum,
            total: (body && body.total) || 0,
          },
        },
      });
      // 回填list数据
      dispatch({
        type: 'SET_DATA_SOURCE',
        payload: {
          dataSource: list,
        },
      });
    }
  }

  // 改变页码
  function handleTableChange(current: number, pageSize?: number) {
    console.log('页码改变', current, pageSize);
    dispatch({
      type: 'SET_PAGINATION',
      payload: {
        pagination: {
          ...state.pagination,
          current,
          pageSize,
        },
      },
    });
  }

  // useCallback包装请求,缓存依赖,优化组件性能
  const fetchDataWarp = useCallback(fetchData, [
    state.pagination.current,
    state.pagination.pageSize,
  ]);

  const paramsChangeFetch = useCallback(fetchData, [params]);

  useEffect(() => {
    console.log('页码改变,请求方法更新', `noInit: ${noInit}, isInit: ${isInit}`);
    if (!(noInit && isInit)) {
      console.log('执行数据列表更新');
      fetchDataWarp();
    }
    if (isInit) {
      setIsInit(false);
    }
  }, [fetchDataWarp]);

  // 查询参数变化重置页码
  useEffect(() => {
    if (mount) {
      paramsChangeFetch(1);
    }
    setMount(true);
  }, [paramsChangeFetch]);

  return (
    <Fragment>
      <Table
        className={styles.table_wrap}
        columns={owncolumns(fetchData)}
        rowKey={(record: any) => record.id}
        pagination={false}
        dataSource={state.dataSource}
        loading={state.loading}
        {...baseProps}
      />
      <Row style={{ marginTop: 20 }} justify="end">
        <Col>
          <Pagination
            defaultCurrent={1}
            current={state.pagination.current}
            total={state.pagination.total}
            showTotal={total => `总共 ${total} 条数据`}
            showSizeChanger
            onChange={handleTableChange}
          />
        </Col>
      </Row>
    </Fragment>
  );
};
export default useAsyncTable;

组件使用方法

1. 代码示例

import React, { useCallback } from 'react';
import ArgTable from '@/components/Hooks/useAsyncTable';
import getColumnFunc from '@/utils/ColumnFunc';
import { getLogTableData } from '@/server/serverList'

const Page: React.FC<any> = () => {
  const [params, setParams] = useState({});
  
  // 查询方法
  const fetchData = (params: any) => getLogTableData(params);
  
  // 获取表头方法
  const getColumn = getColumnFunc.educationManage.operationLog();
  
  // 获取表头字段(缓存数据)  updateMethod 这里是刷新表格方法,用于表格内部操作调用
  const cbGetColumn = useCallback(updateMethod => getColumn(updateMethod), []);

  return (
    <ArgTable params={params} owncolumns={cbGetColumn} queryAction={fetchData} />
  )
}

简单说明一下,我们封装的表格组件主要接收三个参数 params 查询参数, owncolumns 生成表头函数方法, queryAction 查询表格接口

2. 分模块处理表格表头配置

因为我们有大量的表格表头配置,所以我选择分模块集中配置表头,方便后期维护,然后通过 getColumnFunc 方法来导出表头配置。

这里重点提一下 getColumnFunc 函数,主要是获取生成表头配置的函数,先看一下目录结构

image.png

代码实现如下:

// 生成column函数  index.ts
import educationManage, { IEducationManage } from './modules/educationManage';
import systemManage, { ISystemManage } from './modules/systemManage';
import operationManage, { IOperationManage } from './modules/operationManage';
import educationWorkbench, { IEducationWworkbench } from './modules/educationWorkbench';
import marketingWorkbench, { IMarketingWorkbench } from './modules/marketingWorkbench';
import systemClassManage, { ISystemClassManage } from './modules/systemClassManage';
import financialWorkbench, { IFinancialWorkbench } from './modules/financialWorkbench';

interface IColumnFunc {
  educationManage: IEducationManage;
  systemManage: ISystemManage;
  operationManage: IOperationManage;
  educationWorkbench: IEducationWworkbench;
  marketingWorkbench: IMarketingWorkbench;
  systemClassManage: ISystemClassManage;
  financialWorkbench: IFinancialWorkbench;
}

const columnFunc: IColumnFunc = {
  educationManage,
  educationWorkbench,
  systemManage,
  operationManage,
  marketingWorkbench,
  systemClassManage,
  financialWorkbench,
};

export default columnFunc;

其中一个模块的写法

// 生成column函数 xxx模块
import { ColumnProps } from 'antd/lib/table';
import React, { Fragment } from 'react';
import { formatTime } from '@/utils/utils';

export interface IEducationManage {
  operationLog: () => (updata?: any) => ColumnProps<any>[];
}


const operationLog = () => (): ColumnProps<any>[] => [
  {
    title: '操作类型',
    width: '20%',
    dataIndex: 'actionType',
  },
  {
    title: '操作说明',
    dataIndex: 'msg',
    width: '30%',
  },
  {
    title: '操作人员',
    width: '30%',
    dataIndex: 'userName',
  },
  {
    title: '操作时间',
    dataIndex: 'createDate',
    render: (text: any) => <span>{formatTime(text)}</span>,
  },
];

const educationManage: IEducationManage = {
  operationLog,
};

export default educationManage;

这样我们就基本上实现了一个react hooks表格组件,如果有其他业务需求也可以在此基础上进行修改,如有大佬有更好的实现建议或者问题都可以留言,感谢阅读。

3. 最后效果

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容