likes
comments
collection
share

封装react hook之useTable

作者站长头像
站长
· 阅读数 32

重复的逻辑

在做前端开发中表格的渲染、分页、勾选的功能是十分常见的。在实际开发中我们经常使用 Ant Design for react 的 UI 库。它已经帮我们封装了table表格组件、Pagination分页组件,提升了我们的开发效率。

但是,我们依然需要自己去维护dataSource、pagination、rowSelection等状态。

我们可能会写出如下代码:

import React, {useCallback, useEffect, useState} from "react";
import {Table} from "antd";
import {getTableDataRequset} from "../../api";

const Page = () => {
    const [loading, setLoading] = useState(false);
    const [pageNum, setPageNum] = useState<number>(1);
    const [pageSize, setPageSize] = useState<number>(100);
    const [counts, setCounts] = useState<number>(0);
    const [dataSource, setDataSource] = useState([]);
    const [selectedRowKeys, setSelectedRowKeys] = useState([]);

    // 获取table数据
    const getTableData = useCallback(async (index: number, size: number) => {
        setLoading(true);
        const {result, total} = await getTableDataRequset({pageIndex: index, pageSize: size});
        setDataSource(result);
        setLoading(false);
        setCounts(total);
        setSelectedRowKeys([]);
    }, []);

    const handleChangePage = (current: number, size: number) => {
        setPageNum(current);
        setPageSize(size);
        getTableData(current, size);
    };

    const onSelectChange = (keys) => {
        setSelectedRowKeys(keys);
    };

    const rowSelection = {
        selectedRowKeys,
        onChange: onSelectChange,
    };

    useEffect(() => {
        // 初始化
        getTableData(1, 10);
    }, [getTableData]);

    return (
        <Table
            columns={[]}
            loading={loading}
            pagination={{
                pageSize,
                total: counts,
                current: pageNum,
                onChange: handleChangePage,
                showSizeChanger: true,
                pageSizeOptions: ["10", "20", "50"]
            }}
            rowSelection={rowSelection}
            dataSource={dataSource}
        />
    );
};

而如果包含关键字搜索功能,我们在搜索的时候,还需要重置一下当前页数、勾选项等状态...

在删除表格数据后,刷新表格是否刷新当前页,还是前一页(当前页数据删除完的情况下,需要刷新的是前一页)

...

我们可以发现这些逻辑是重复的、且是不可少的。我们可以使用hooks封装起来。减少这样的重复代码,提升效率,让我们更加专注在业务开发上。

设计一个useTalbe

  • useTable主要用于封装常用的分页逻辑、表格勾选逻辑、表格数据请求逻辑。
  • useTable接收2个参数,requsetService是表格列表的promise请求函数、options是useTable的配置对象
  • useTable会返回tableProps、tableSelectRows、table状态的一些操作函数: searchTable clearTable clearSelectedRows refreshTable
    const options = {
        isInitTable: false,
        initRequsetParams: {},
        isRememberSelect: false
    }
    const {tableProps, searchTable} = useTable(getTableList, options);

requsetService

useTable接收的第一个参数,表格列表的promise请求函数

// 接口返回的数据结构
interface Result<RecordType=any> {
    /** 分页列表 */
    result: RecordType[];
    /** 总数 */
    totalCount: number;
}

// 分页参数字段
interface ReuqsetParams {
    /** 分页器之外的其他字段 */
    [key: string]: any;
    /** 当前页码 */
    pageIndex: number;
    /** 每页数量 */
    pageSize: number;
}

type RequsetService = (params?: ReuqsetParams) => Promise<Result>;

options

useTable接收的第二个参数,optonsuseTable的配置对象

interface Opitons {
    /** 是否初始化table,默认为false,主动请求使用searchTable */
    isInitTable?: boolean;
    /** 请求初始化参数 */
    initRequsetParams?: object; 
    /** 翻页是否记住勾选的状态 */
    isRememberSelect?: boolean;
}

返回值

interface SearchOptions {
    /** 是否缓存请求参数的状态,默认true */
    isCacheParams: boolean;
}

interface ReturnTable<RecordType, Params> {
    /** Table组件需要的的props,直接传给Table组件 */
    tableProps: {
        dataSource: RecordType[];
        loading: boolean;
        pagination: TablePaginationConfig;
        rowSelection: TableRowSelection<RecordType>;
    };
    /** 选中的行 */
    tableSelectRows: RecordType[];
    /** 接收搜索关键字搜索列表,分页会重置到第一页 */
    searchTable: (params?: Params, searchOptions?: SearchOptions) => void;
    /**
      根据上次请求参数,刷新当前页列表。(适用于新增、编辑、删除刷新列表)
      removeCount(默认0) 为删除的数量,删除列表数据时,如果当前页删除完需要请求上一页
    */
    refreshTable: (options?: {removeCount?: number}) => void;
    /** 清空表格数据方法 */
    clearTable: () => void;
    /** 清除选中项方法 */
    clearSelectedRows: () => void;
}

使用场景

只需要分页器,不需要勾选项

const View = () => {
    const {
      tableProps,
      searchTable,
    } = useTable(getTableList);

    // 初始化表格
    useEffect(() => {
      searchTable();
    }, [searchTable]);
  
    return (
        <>
            <Table
                {...tableProps}
                // 设置为null
                rowSelection={null}
                columns={tableColumns}
            />
        </>
    )
};

使用searchTable和refreshTable刷新表格

const View = () => {
    // 返回了table的状态及操作方法,根据需求使用
    const {
      tableProps,
      tableSelectRows,
      searchTable,
      refreshTable,
      clearTable,
      clearSelectedRows
    } = useTable(getTableList);

    // 刷新列表
    // 会使用上一次的搜索参数
    const handleRefresh = () => {
        refreshTable();
    };

    // 搜索列表
    // 传入搜索参数
    const handleSearch = () => {
        searchTable({
            keyword: "111",
            projectId: 1,
            // ...
        });
        // xxx.api?keyword=111&projectId=1
    };

    // 初始化表格
    useEffect(() => {
      searchTable();
    }, [searchTable]);
  
    return (
        <>
            <button onClick={handleRefresh}>刷新列表</button>
            <button onClick={handleSearch}>搜索列表</button>
            <Table
                {...tableProps}
                columns={tableColumns}
            />
        </>
    )
};

实现useTalbe

我们根据设计的api实现一下该hook utils文件

// 获取勾选项(翻页记忆)
export const getSelectedRows = <T>(curPageSelectedKeys: rowKey[], curPageData: T[], selectedRows, rowKey = "id"): Result<T> => {
  const dataMap = new Map<string|number, T>([]);
  const nextRows = [];
  // 当前页勾选项 + 其他分页已勾选项
  curPageData.forEach((item) => {
    dataMap.set(item[rowKey], item);
    if (curPageSelectedKeys.includes(item[rowKey])) {
      // 当前页勾选项
      nextRows.push(item);
    }
  });
  selectedRows.forEach((item) => {
    if (dataMap.get(item[rowKey]) === undefined) {
      // 其他分页已勾选项
      nextRows.push(item);
    }
  });
  const isAll = curPageSelectedKeys.length === curPageData.length;
  return {
    selectedRows: nextRows,
    selectedKeys: nextRows.map((item) => item[rowKey]),
    isAllSelected: isAll
  };
};

useTable文件

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react-hooks/exhaustive-deps */
import React, {useEffect, useState, useRef, useCallback} from "react";
import {TablePaginationConfig} from "antd/lib/table";
import ReactDOM from "react-dom";
import {getSelectedRows} from "../../utils";

interface Result<RecordType> {
    result: RecordType[];
    totalCount: number;
}

type Service = (params?: object) => Promise<Result<any>>;

type Key = React.Key;

// 提取列表项的类型
type ServiceResponse<S> = S extends (params?: any) => Promise<infer Response>
    ? Response extends Result<infer RecordType>
        ? RecordType
        : any
    : never;

// 提取接口请求参数的类型
type ServiceParams<S> = S extends (params: infer Params) => Promise<any>
    ? Partial<Omit<Params, "pageIndex" | "pageSize">>
    : never;

interface Opitons<Params> {
    isInitTable?: boolean;
    initRequsetParams?: Params;
    isRememberSelect?: boolean;
}

interface Selected<RecordType> {
    rows: RecordType[];
    rowKeys: Key[];
}

export interface TableRowSelection<RecordType> {
    selectedRowKeys?: Key[];
    onChange?: (selectedRowKeys: Key[], selectedRows: RecordType[]) => void;
}

interface SearchOptions {
    isCacheParams: boolean;
}

interface ReturnTable<RecordType, Params> {
    tableProps: {
        dataSource: RecordType[];
        loading: boolean;
        pagination: TablePaginationConfig;
        rowSelection: TableRowSelection<RecordType>;
    };
    searchTable: (params?: Params, searchOptions?: SearchOptions) => void;
    clearTable: () => void;
    clearSelectedRows: () => void;
    refreshTable: (options?: {removeCount?: number}) => void;
    tableSelectRows: RecordType[];
}

const defaultPagination = {
    current: 1,
    pageSize: 10,
    total: 0,
};

const defaultSelected = {
    rows: [],
    rowKeys: [],
};

const defalutOptions = {
    isInitTable: false, // 是否初始化table
    initRequsetParams: {},
    isRememberSelect: false, // 是否记忆翻页勾选
};

/**
 * @template S
 * @param {Service} service 请求接口
 * @param {Opitons} [options=defalutOptions] 初始化参数
 * @return {*}  {ReturnTable<RecordType, Params>}
 */
const useTable = <S extends Service, RecordType = ServiceResponse<S>, Params = ServiceParams<S>>(
    service: S,
    options: Opitons<Params> = defalutOptions as Opitons<Params>
): ReturnTable<RecordType, Params> => {
    const [isUpdate, setIsUpdate] = useState(false); // 是否更新数据
    const [dataSource, setDataSource] = useState<RecordType[]>([]);
    const [loading, setLoading] = useState(false);
    const [pagination, setPagination] = useState(defaultPagination); // 分页配置参数
    const [selected, setSelected] = useState<Selected<RecordType>>(defaultSelected); // 选择的条目
    const requsetParams = useRef(options.initRequsetParams); // 请求参数
    const isInitTable = useRef(options.isInitTable); // 是否初始化table
    const isRememberSelect = useRef(options.isRememberSelect); // 是否记忆翻页勾选
    const requsetService = useRef(service); // 分页列表请求

    // 接收请求参数,根据参数搜索列表,初始化分页器
    const searchTable = useCallback((
        searchParams: Params = {} as Params,
        searchOptions: SearchOptions = {isCacheParams: true}
    ) => {
        if (!isInitTable.current) {
            isInitTable.current = true;
        }
        // 使用上一次请求的缓存
        if (searchOptions.isCacheParams) {
            requsetParams.current = {
                ...requsetParams.current,
                ...searchParams
            };
        } else {
            requsetParams.current = {
                ...searchParams
            };
        }

        ReactDOM.unstable_batchedUpdates(() => {
            // 在非第一页 跳转到第1页 进行搜索
            setPagination((prevState) => ({
                ...prevState,
                current: defaultPagination.current,
            }));

            // 强制刷新
            setIsUpdate((prevState) => !prevState);
        });
    }, []);

    // // 根据上次请求参数,刷新当前页列表
    const refreshTable = useCallback((refreshOptions = {removeCount: 0}) => {
        const {removeCount} = refreshOptions;
        // 总数量-删除数量 /每页条数 = 总页 >= 当前页 刷新当前页
        const afterTotalPage = Math.ceil((pagination.total - removeCount) / pagination.pageSize);
        if (afterTotalPage >= pagination.current || afterTotalPage === 0) {
            setIsUpdate((prevState) => !prevState);
        } else {
            ReactDOM.unstable_batchedUpdates(() => {
                setPagination((prevState) => ({
                    ...prevState,
                    current: afterTotalPage,
                }));
                // 强制刷新
                setIsUpdate((prevState) => !prevState);
            });
        }
    }, [pagination]);

    // 清空列表
    const clearTable = useCallback(() => {
        setDataSource([]);
        setPagination(defaultPagination);
        setSelected(defaultSelected);
    }, []);

    // 清除选中项
    const clearSelectedRows = useCallback(() => {
        setSelected(defaultSelected);
    }, []);

    // 翻页
    const onPageChange = (page: number, pageSize: number) => {
        setPagination({
            ...pagination,
            current: page,
            pageSize,
        });
    };

    // 表格选择
    const onSelectChange = (selectedRowKeys: Key[], selectedRows: RecordType[]) => {
        if (isRememberSelect.current) {
            const {
                selectedRows: rememberSelectedRows,
                selectedKeys: rememberSelectedkeys,
            } = getSelectedRows(selectedRowKeys, dataSource, selected.rows);

            setSelected({
                rows: rememberSelectedRows,
                rowKeys: rememberSelectedkeys,
            });
        } else {
            setSelected({
                rows: selectedRows,
                rowKeys: selectedRowKeys,
            });
        }
    };

    const rowSelection = {
        selectedRowKeys: selected.rowKeys,
        onChange: onSelectChange,
    };

    const paginationConfig = {
        ...pagination,
        position: ["bottomRight" as "bottomRight"],
        pageSizeOptions: ["10", "20", "30", "40", "50"],
        showSizeChanger: true,
        showQuickJumper: true,
        onChange: onPageChange,
        showTotal(totals: number, _range: [number, number]) {
            return `共 ${totals}条 , 共 ${Math.ceil(totals / pagination.pageSize)}页`;
        },
    };
    useEffect(() => {
        if (!isInitTable.current) {
            return;
        }
        setLoading(true);
        const tableRequsetParams = {
            ...requsetParams.current,
            pageIndex: pagination.current,
            pageSize: pagination.pageSize
        };

        if (!isRememberSelect.current) {
            // 初始化选择器
            setSelected(defaultSelected);
        }

        requsetService.current(tableRequsetParams)
            .then((res) => {
                setLoading(false);
                setDataSource(res.result);
                setPagination({
                    ...pagination,
                    total: res.totalCount,
                });
            })
            .catch((err) => {
                setLoading(false);
            });
    }, [pagination.current, pagination.pageSize, isUpdate]);

    return {
        tableProps: {
            dataSource,
            loading,
            pagination: paginationConfig,
            rowSelection
        },
        searchTable,
        refreshTable,
        clearTable,
        clearSelectedRows,
        tableSelectRows: selected.rows
    };
};

export default useTable;

转载自:https://juejin.cn/post/7232460999189708861
评论
请登录