表格Table常用逻辑hooks封装,自用,基于AntD-React

先看最终代码:

import React, { useState, useMemo, useEffect } from 'react'

interface PageAbout {
  current: number
  pageSize: number
}
// 配合antd的table,生产table所需函数方法及state
export function useTableData<
  TableParams extends SearchData & PageAbout = any,
  SearchData extends object = any,
  Datasource = any
>({
  tableParamsInit = { pageSize: 10, current: 1 } as any,
  searchDataInit = {},
  pullData = () => {
    // console.log('pullData')
  },
  getTotal = () => {
    return 1
  },
}: {
  tableParamsInit?: TableParams
  searchDataInit?: SearchData | {}
  // 调用接口的方法,接受setDatasource作为参数, 在获取数据后,通过其将数据注回useTableData
  pullData: (
    params: TableParams,
    setDatasource: React.Dispatch<React.SetStateAction<Datasource[] | []>>
  ) => void
  // 从结果中获取数据总数
  getTotal?: (res: any) => number
}) {
  const [datasource, setDatasource] = useState<Datasource[] | []>([])
  const [tableParams, setTableParams] = useState<TableParams>(tableParamsInit)
  const [searchData, setSearchData] = useState<SearchData | {}>(searchDataInit)
  const [total, setTotal] = useState(1)
  //表格接口依赖变动,调用接口,拉取新的数据
  useEffect(() => {
    const res = pullData(tableParams, setDatasource)
    setTotal(getTotal(res))
  }, [tableParams])
  // 搜索时,将搜索state set至表格接口依赖,表格接口依赖变化,自动触发接口调用
  const handleSearch = () => {
    setTableParams((s) => ({ ...s, current: 1, ...searchData }))
  }
  // 重置,将搜索依赖及表格接口依赖重置为init
  const handleReset = () => {
    setTableParams(tableParamsInit)
    setSearchData(searchDataInit)
  }
  // 页码变更
  const handlePageChange = (v: number) => {
    setTableParams((s) => ({ ...s, current: v }))
  }
  // 执行删除操作,数据源出现变动,如果删除的时当前页最后一条数据,则退回有数据的页数,当前页为第一页则只刷新
  // count删除的数据量
  const handleAfterDel = (count?: number) => {
    // 第一页时,直接获取新数据
    if (datasource?.length > (count ?? 1) || tableParams.current === 1) {
      const res = pullData(tableParams, setDatasource)
      setTotal(getTotal(res))
    } else {
      // 被删除的页数,用于计算需要返回的页数
      const deletedPage = Math.floor(
        Number(
          (
            ((count ?? 1) - (datasource?.length ?? 0)) /
            tableParams.pageSize
          ).toFixed(0)
        )
      )
      setTableParams((s) => ({
        ...s,
        current: s.current - deletedPage > 1 ? s.current - deletedPage : 1,
      }))
    }
  }
  return {
    // 数据总数
    total,
    setTotal,
    // 表格数据源
    datasource,
    setDatasource,
    // 表格接口依赖
    tableParams,
    setTableParams,
    // 搜索依赖state
    searchData,
    setSearchData,
    // 搜索操作
    handleSearch,
    // 重置操作
    handleReset,
    // 页面变化
    handlePageChange,
    // 删除数据后执行,参数为删除的条数
    handleAfterDel,
  }
}

封装思路

hooks提供了以往react以往风格不方便实现的逻辑复用能力,笔者使用hooks于生产实践中也有近俩月了,写的后台项目中非常多的表格,遂基于antd的table组件,封装了配套的可复用的逻辑。

  • 参数部分

一般来说,所有表格将会出现的不同部分需要作为参数传入hooks中。
1. get表格数据所需的params参数初始值:tableParamsInit (默认需要翻页操作)
2. 查询、筛选、搜索等功能所依赖的参数初始值:searchDataInit
3. 调用接口,获取表格数据的方法
4. 获取数据总数的方法(如果后台的接口格式几乎一样,这个也可以写死,不用传入)

{
  tableParamsInit = { pageSize: 10, current: 1 } as any,
  searchDataInit = {},
  pullData = () => {
    // console.log('pullData')
  },
  getTotal = () => {
    return 1
  },
}: {
  tableParamsInit?: TableParams
  searchDataInit?: SearchData | {}
  // 调用接口的方法,接受setDatasource作为参数, 在获取数据后,通过其将数据注回useTableData
  pullData: (
    params: TableParams,
    setDatasource: React.Dispatch<React.SetStateAction<Datasource[] | []>>
  ) => void
  // 从结果中获取数据总数
  getTotal?: (res: any) => number
}
  • 状态state
    1.dataSource用来存储表格的数据
    2.tableParams是用于发送接口请求的最终参数
    3.searchData是查询状态的state
  const [datasource, setDatasource] = useState<Datasource[] | []>([])
  const [tableParams, setTableParams] = useState<TableParams>(tableParamsInit)
  const [searchData, setSearchData] = useState<SearchData | {}>(searchDataInit)
  • 监听effect
    • 监听tableParams即接口参数的变化,接口依赖的参数变了就请求接口获取新的数据,这种调用接口的时机是实际生产当中笔者总结出的逻辑比较清晰易懂不容易出错的一种。
    • 值得注意的是,用于查询搜索的那些参数,只有当用户点击查询或者搜索按钮时,才会被添加到tableParams中,触发接口获取数据。如果读者在生产中,产品要求搜索内容变化就要立即获取数据的话,则应该把搜索所依赖的参数直接收纳于tableParams中,而不应当再作为searchData中的内容。
 useEffect(() => {
   const res = pullData(tableParams, setDatasource)
   setTotal(getTotal(res))
 }, [tableParams])
  • 方法handler
    1. handleSearch 这个方法一般被笔者直接添加到查询的按钮上,通过把searchData的值赋值到tableParams中,改变tableParams,触发pullData获取新的数据。
      注意:current也被笔者重新赋值为1,是因为,查询或筛选后的数据一般会少于总数据。这样的话如果不把页数重置为1则会出现查询到的结果不符合预期的情况。
  // 搜索时,将搜索state set至表格接口依赖,表格接口依赖变化,自动触发接口调用
  const handleSearch = () => {
    setTableParams((s) => ({ ...s, current: 1, ...searchData }))
  }

    1. handleReset 这个方法一般被笔者放在重置按钮上,通过把tableParams及searchData的值重新赋值为init的值,触发pullData,覆盖原有的数据。
  // 重置,将搜索依赖及表格接口依赖重置为init
  const handleReset = () => {
    setTableParams(tableParamsInit)
    setSearchData(searchDataInit)
  }
    1. handlePageChange 翻页,直接给antD的Table组件的pagination 的onChange上就ok
  // 页码变更
  const handlePageChange = (v: number) => {
    setTableParams((s) => ({ ...s, current: v }))
  }
    1. handleAfterDel 当表格具有删除数据的功能时,不得不考虑删除数据后的一些处理逻辑。
    • 第一页时,用当前参数直接获取新数据
    • 当前数据量大于被删除的数据量,用当前参数直接获取新数据
    • 计算出被删除的页数,以及应该具有数据的最后一页,出现0或者负则应该为第一页
  // 执行删除操作,数据源出现变动,如果删除的时当前页最后一条数据,则退回有数据的页数,当前页为第一页则只刷新
// count删除的数据量
  const handleAfterDel = (count?: number) => {
    // 第一页时,直接获取新数据
    if (datasource?.length > (count ?? 1) || tableParams.current === 1) {
      const res = pullData(tableParams, setDatasource)
      setTotal(getTotal(res))
    } else {
      // 被删除的页数,用于计算需要返回的页数
      const deletedPage = Math.floor(
        Number(
          (
            ((count ?? 1) - (datasource?.length ?? 0)) /
            tableParams.pageSize
          ).toFixed(0)
        )
      )
      setTableParams((s) => ({
        ...s,
        current: s.current - deletedPage > 1 ? s.current - deletedPage : 1,
      }))
    }
  }
  • 使用
...

  const {
    total,
    setTotal,
    // 表格数据源
    datasource,
    setDatasource,
    // 表格接口依赖
    tableParams,
    setTableParams,
    // 搜索依赖state
    searchData,
    setSearchData,
    // 搜索操作
    handleSearch,
    // 重置操作
    handleReset,
    // 页面变化
    handlePageChange,
    // 删除数据后执行,参数为删除的条数
    handleAfterDel,
  } = useTableData<any, any, any>({
    tableParamsInit: { pageSize: 10, current: 1 },
    searchDataInit: { discount_type: 0 },
    getTotal() {
      return 100
    },
    pullData({ pageSize: limit, current: page, ...rest }, setData) {
      dispatch({
        type: `${namespace}/${ActionTypes.publicCodeGet}`,
        payload: {
          params: {
            limit,
            page,
            ...rest,
          },
          onSuccess(res) {
            setData(res?.data)
          },
        },
      })
      setData(MOCK.publicCodeTableData)
    },
  })

return <>  
      <Form className={`${styles.search} mt20`} layout="inline">
          <Form.Item label="通用码名称">
            <Input
              placeholder="请输入通用码名称"
              value={searchData.keywords}
              onPressEnter={handleSearch}
              onChange={({ target: { value } }) => {
                setSearchData((s) => ({ ...s, keywords: value }))
              }}
            ></Input>
          </Form.Item>
          <Form.Item label="优惠方式">
            <div style={{ width: 180 }}>
              <Select
                className={`w100`}
                placeholder="请选择优惠方式"
                options={[{ label: '全部', value: 0 }, ...types.discount.list]}
                value={searchData.discount_type}
                onChange={(v) => {
                  setSearchData((s) => ({ ...s, discount_type: v }))
                }}
              ></Select>
            </div>
          </Form.Item>
          <Form.Item>
            <Button onClick={handleSearch} type="primary">
              查询
            </Button>
          </Form.Item>
          <Form.Item>
            <Button type="ghost" onClick={handleReset}>
              重置
            </Button>
          </Form.Item>
        </Form>

        <Table
          loading={tableLoading}
          dataSource={datasource}
          rowKey="id"
          pagination={{
              total,
              pageSize: tableParams.pageSize,
              current: tableParams.current,
              showTotal: (total) => `共有${total}条数据`,
              showSizeChanger: false,
              onChange: handlePageChange,
          }}
          columns={[
            { title: '通用码名称', dataIndex: 'name' },
            {
              title: '适用范围',
              dataIndex: 'range',
              render(_, record) {
                return (
                  <div>
                    <div className={`p16`}>
                      {types.goods.data[record.range.goods_type]}
                    </div>
                    <div className={`p12g`}>{record.range.name}</div>
                  </div>
                )
              },
            },
            { title: '优惠码', dataIndex: 'code' },
            { title: '优惠方式', align: 'center', dataIndex: 'path' },
            { title: '可用数量', align: 'center', dataIndex: 'count' },
            {
              title: '有效期',
              dataIndex: 'time',
              render(time) {
                return (
                  <div>
                    <div>起:{time[0]}</div>
                    <div>止:{time[1]}</div>
                  </div>
                )
              },
            },
            {
              title: '状态',
              align: 'center',
              dataIndex: 'is_enable',
              render(text) {
                return text === 1 ? '启用' : '禁用'
              },
            },
            {
              title: '操作',
              dataIndex: 'option',
              align: 'center',
              key: 'option',
              width: 120,
              render: (text: any, record: any, index) => (
                <span className={styles.operate}>
                  <a
                    onClick={() => {
                      router.push({
                        pathname: '/marketing/publicCode/form',
                        query: { id: record.id ?? '' },
                      })
                    }}
                  >
                    编辑
                  </a>
                  <Divider type="vertical" />
                  <Popover
                    placement="bottom"
                    content={
                      <div>
                        <p
                          className="hoverActive cp pb4"
                          onClick={() => {
                            router.push({
                              pathname: '/marketing/publicCode/detail',
                              query: { id: record.id },
                            })
                          }}
                        >
                          使用情况
                        </p>
                        <p
                          className="hoverActive cp pb4"
                          onClick={() => {
                            // handleDel(record.id);
                            handleDel(record.id, index)
                          }}
                        >
                          删除
                        </p>
                      </div>
                    }
                    trigger="hover"
                  >
                    <a onClick={() => {}}>更多</a>
                  </Popover>
                </span>
              ),
            },
          ]}
       / >
  </>
...

总结:类型检查等处还有待优化。笔者水平有限,还请各位读者大佬斧正。

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