自定义 hooks 的思考

880 阅读8分钟

一. 自定义hooks-驱动条件

hooks本质上是一个函数。函数的执行,决定与无状态组件组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就会执行自定义hooks的执行,由此可见组件本身执行和hooks的执行如出一辙。

那么prop的修改,useState,useReducer使用是无状态组件更新条件,那么就是驱动hooks执行的条件。我们用一幅图来表示如上关系。

9.png

二. 自定义hooks-通用模式

我们设计的自定义react-hooks应该是长的这样的。

10.png

const [ xxx , ... ] = useXXX(参数A,参数B...)

在我们在编写自定义hooks的时候,要特别~特别~特别关注的是传进去什么返回什么。返回的东西是我们真正需要的。更像一个工厂,把原材料加工,最后返回我们。正如上图所示

三. 自定义hooks实战

1. useLineEchart

1.png

  • 遇到的问题:

    • 【活动详情】页面中存在大量相同的 echart 折现图
    • 避免重复去配置 echart 折线图的配置项
    • 多人开发相同页面,需要保证 echart 的视觉效果 UI 层次一致
  • 解决方法:

    统一将接口数据转换,echart 折线图配置项抽离,书写自定义hooks useLineEchart

  • useLineEchart 具体设计思路:

    • 传入参数接口:

      interface YAxisKey {
        key: string;    // y 轴对应的接口返回字段
        legend: string; // y 轴对应的接口返回字段文案描述
      }
      
      interface LineEchartProps {
        dataSource: Array<DataSourceItem>; // 数据源
        xAxisKey: string; // x 轴的 key 名
        yAxisKeys: Array<YAxisKey>; // y 轴的 [{ key: 'pv',  legend: '次数' }]
        isTime?: boolean; // x 轴是否为时间
      }
      
      export const useLineEchart = ({ dataSource, xAxisKey, yAxisKeys, isTime = true }: LineEchartProps) => {
      	...
      }
      
    • 获取 echart 的 legend.data 配置项数据

      export const useLineEchart = ({ dataSource, xAxisKey, yAxisKeys, isTime = true }: LineEchartProps) => {
      	...
      
      	const legendData: string[] = useMemo(() => yAxisKeys.map(i => i.legend), [yAxisKeys]);	
      
      	...
      }
      
    • 获取 xAxis.data 的数据

      export const useLineEchart = ({ dataSource, xAxisKey, yAxisKeys, isTime = true }: LineEchartProps) => {
      	...
      
      	const xAxisData: (string | number)[] = useMemo(
          () => dataSource.map(i => (isTime ? moment(i[xAxisKey] as number).format('YYYY-MM-DD') : i[xAxisKey])),
          [yAxisKeys],
        );
      
      	...
      }
      
    • 获取系列 series 数据

      export const useLineEchart = ({ dataSource, xAxisKey, yAxisKeys, isTime = true }: LineEchartProps) => {
      	...
      
      	const seriesData: SeriesItem[] = useMemo(
          () =>
            yAxisKeys.map(({ key, legend: name }) => {
              return {
                name,
                type: 'line',
                stack: 'Total',
                data: dataSource.map(i => i[key] as number),
              };
            }),
          [yAxisKeys, dataSource],
        );
      
      	...
      }
      
    • 修改 echart 配置项,并返回

      export const useLineEchart = ({ dataSource, xAxisKey, yAxisKeys, isTime = true }: LineEchartProps) => {
      	...
      
      	useEffect(() => {
          const option = {
            tooltip: {
              trigger: 'axis',
            },
            legend: {
              data: legendData,
            },
            grid: {
              left: '0%',
              right: '3%',
              bottom: '3%',
              containLabel: true,
            },
            xAxis: {
              type: 'category',
              boundaryGap: false,
              data: xAxisData,
            },
            yAxis: {
              type: 'value',
            },
            series: seriesData,
          };
          setLineChartOption(option);
        }, [legendData, xAxisData, seriesData]);
      
        return {
          lineChartOption,
        };
      }
      
  • useLineEchart Demo 实例:

    import React from 'react';
    import { IRouteComponentProps } from 'umi';
    import { Typography } from 'antd';
    import ReactEcharts from 'echarts-for-react';
    import { useLineEchart } from './hooks/useLineEchart';
    
    export interface LineEchartDemoProps extends IRouteComponentProps {}
    
    export default function LineEchartDemo(props: LineEchartDemoProps) {
      const { lineChartOption } = useLineEchart({
        dataSource: [
          { time: 1637366400000, cy_pv: 122, cy_uv: 533, cg_pv: 223, cg_uv: 20234, ff_pv: 12 },
          { time: 1637452800000, cy_pv: 222, cy_uv: 1833, cg_pv: 523, cg_uv: 2460, ff_pv: 1423 },
          { time: 1637539200000, cy_pv: 333, cy_uv: 1333, cg_pv: 243, cg_uv: 260, ff_pv: 144 },
          { time: 1637625600000, cy_pv: 444, cy_uv: 1533, cg_pv: 236, cg_uv: 2046, ff_pv: 155 },
          { time: 1637712000000, cy_pv: 555, cy_uv: 12733, cg_pv: 2364, cg_uv: 205, ff_pv: 16 },
          { time: 1637798400000, cy_pv: 666, cy_uv: 123, cg_pv: 24453, cg_uv: 520, ff_pv: 31 },
          { time: 1637884800000, cy_pv: 777, cy_uv: 12335, cg_pv: 2663, cg_uv: 1420, ff_pv: 1443 },
        ],
        xAxisKey: 'time',
        yAxisKeys: [
          { key: 'cy_pv', legend: '参与抽奖人数' },
          { key: 'cy_uv', legend: '参与抽奖次数' },
          { key: 'cg_pv', legend: '成功抽奖人数' },
          { key: 'cg_uv', legend: '成功抽奖次数' },
          { key: 'ff_pv', legend: '发放抽奖机会数量' },
        ],
      });
    
    return (
        <div className="echart-demo-container">
          <div className="echart-content">
            <ReactEcharts option={lineChartOption} notMerge={true} lazyUpdate={true} />
          </div>
        </div>
      );
    }
    

    2.png

2. useTable

  • 遇到的问题:

    • antd table 组件动态表头合并

5.png

-   *antd table 组件动态表单项合并*

4.png

-   *antd table 组件表头 hover 状态,显示表头说明*

3.png

  • 解决方法:

    统一将接口数据转换成新的 dataSource,动态配置 columns 配置项,hover 表头说明使用 React.ReactNode 就行渲染。

  • useTable 设计思想:

    • 传入参数接口定义:

      import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react';
      import { Tooltip, Icon } from 'antd';
      import { ColumnProps } from 'antd/es/table';
      import './useTable.less';
      
      /** 需要合并的表,,请求传入的 child 数据对象 */
      interface ChileHeader {
        key: string;
        name: string;
        value: string;
      }
      
      /** 存取需要合并的表头 dataSource 数据源,并将其 children 数组存在一个 key 为其请求字段的字典 */
      interface ChildrenHeaderMap {
        [index: string]: ColumnProps<User>[];
      }
      
      /** dataSource 数据源定义 */
      interface User {
        [index: string]: string | number | ChileHeader[];
        key: number;
        name: string;
      }
      
      /** 表头定义 */
      interface HeaderMap {
        key: string; // 每一列的接口请求字段 key
        name: string; // 每一列的接口请求字段 key 的文案说明
        desc?: string; // hover 悬浮提示
        isMergedKey?: boolean; // 是否是合并的行
        isMergedHeader?: boolean; // 是否是需要合并的表头
      }
      
      interface TableProps {
        dataSource: Array<User>; // 数据源
        headerMap: HeaderMap[]; // 表头字典
      }
      
      /**
       * 解决问题:
       * * antd table 组件动态表头合并
       * * antd table 组件动态表单项合并
       * * antd table 组件表头 hover 状态,显示表头说明
       */
      export const useTable = ({ dataSource, headerMap }: TableProps) => {
      	...
      }
      
    • 获取需要合并的行:

      export const useTable = ({ dataSource, headerMap }: TableProps) => {
      	...
      
      	const mergedKeys: string[] = useMemo(() => headerMap.filter(i => i.isMergedKey).map(i => i.key), [headerMap]);
      
      	...
      }
      
    • 获取需要合并的表头:

      export const useTable = ({ dataSource, headerMap }: TableProps) => {
      	...
      
      	const mergedHeaders: string[] = useMemo(() => headerMap.filter(i => i.isMergedHeader).map(i => i.key), [headerMap]);
      
      	...
      }
      
    • 修改需要合并的表头 dataSource 数据源,并将其 children 数组存在一个 key 为其请求字段的字典中:

      export const useTable = ({ dataSource, headerMap }: TableProps) => {
      	...
      
      	const childrenHeaderMap: React.MutableRefObject<ChildrenHeaderMap> = useRef({});
      
        const mergedHeaderSource: Array<User> = useMemo(() => {
          if (mergedHeaders.length <= 0) {
            return dataSource || [];
          }
          const source: Array<User> = dataSource.map(i => {
            for (const key in i) {
              if (Object.prototype.hasOwnProperty.call(i, key)) {
                if (mergedHeaders.includes(key)) {
                  childrenHeaderMap.current[key] = (i[key] as Array<ChileHeader>).map(({ key: _key, value, name: title }) => {
                    i[_key] = value;
                    return { title, dataIndex: _key, key: _key };
                  });
                }
              }
            }
            return i;
          });
          return source;
        }, [dataSource, mergedHeaders]);
      
      	...
      }
      
    • 归并 dataSource 数据源:

      export const useTable = ({ dataSource, headerMap }: TableProps) => {
      	...
      
      	/** 归并数据 */
        const classifyRows: Array<User> = useMemo(() => {
          if (mergedKeys.length <= 0 || dataSource.length <= 0) {
            return mergedHeaderSource || [];
          }
          return mergedHeaderSource.slice(1).reduce(
            (ordered, row) => {
              const index = ordered.findIndex(orderedRow => orderedRow[mergedKeys[0]] === row[mergedKeys[0]]);
              if (index !== -1) {
                return [...ordered.slice(0, index + 1), row, ...ordered.slice(index + 1)];
              }
              return [...ordered, row];
            },
            [mergedHeaderSource[0]],
          );
        }, [mergedHeaderSource, mergedKeys]);
      
      	...
      }
      
    • 合并行:

      /** 计算归并列表项特定key值的和 */
        const calcTotal: (mergedRows: User[], currentRow: User, idx: number) => void = useCallback(
          (mergedRows: User[], currentRow: User, idx: number) => {
            if (mergedKeys.length <= 1) {
              return;
            }
            for (let i = 1; i < mergedKeys.length; i++) {
              const key = mergedKeys[i];
              mergedRows[idx][key] = (+mergedRows[idx][key] as number) + (+currentRow[key] as number);
            }
          },
          [mergedKeys],
        );
      
        /** 合并列表项 */
        const mergeRows: Array<User> = useMemo(() => {
          if (mergedKeys.length <= 0 || classifyRows.length <= 0) {
            return classifyRows || [];
          }
          classifyRows[0].rowSpan = 1;
          let idx = 0;
          return classifyRows.slice(1).reduce(
            (mergedRows, currentRow, index) => {
              if (currentRow[mergedKeys[0]] === mergedRows[idx][mergedKeys[0]]) {
                (mergedRows[idx].rowSpan as number)++;
                currentRow.colSpan = 0;
                calcTotal(mergedRows, currentRow, idx);
              } else {
                currentRow.rowSpan = 1;
                idx = index + 1;
              }
              return [...mergedRows, currentRow];
            },
            [classifyRows[0]],
          );
        }, [classifyRows, mergedKeys]);
      
    • 生成表单 columns 配置项,并输出结果:

      /** hover 悬浮提示 */
      const TooltipTitle = ({ text, title }: { text: string; title: string }) => {
        return (
          <React.Fragment>
            <span style={{ marginRight: 8 }}>{text}</span>
            <Tooltip placement="bottom" title={title}>
              <Icon type="question-circle" theme="outlined" />
            </Tooltip>
          </React.Fragment>
        );
      };
      
      export const useTable = ({ dataSource, headerMap }: TableProps) => {
      	... 
      
      	useEffect(() => {
          const getColumns: () => ColumnProps<User>[] = () => {
            const _columns: ColumnProps<User>[] = [];
            headerMap.forEach(({ key, name, desc, isMergedHeader }) => {
              const columnItem: ColumnProps<User> = {};
              columnItem.title = desc ? <TooltipTitle text={name} title={desc} /> : name;
              columnItem.dataIndex = key;
              if (!isMergedHeader) {
                columnItem.align = 'center';
                columnItem.render = (text, record) => {
                  if (mergedKeys.length > 0 && mergedKeys.includes(key)) {
                    return {
                      children: text,
                      props: {
                        rowSpan: record.rowSpan,
                        colSpan: record.colSpan,
                      },
                    };
                  }
                  return text;
                };
              } else {
                columnItem.children = childrenHeaderMap.current[key];
              }
              _columns.push(columnItem);
            });
            return _columns;
          };
          setColumns(getColumns());
        }, [dataSource, headerMap, mergedKeys]);
      
        return {
          columns,
          _dataSource,
        };	
      }
      
  • useTable Demo 实例:

    • 动态合并表头和表单项的Table

      ....
      const { columns, _dataSource } = useTable({
          dataSource: [
            {
              key: 1,
              name: '抽奖1',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
              lb_pv: [
                {
                  key: 'lb_pv1',
                  name: '礼包1',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv2',
                  name: '礼包2',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv3',
                  name: '礼包3',
                  value: '123/1232',
                },
              ],
            },
            {
              key: 3,
              name: '抽奖2',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
              lb_pv: [
                {
                  key: 'lb_pv1',
                  name: '礼包1',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv2',
                  name: '礼包2',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv3',
                  name: '礼包3',
                  value: '123/1232',
                },
              ],
            },
            {
              key: 2,
              name: '抽奖3',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
              lb_pv: [
                {
                  key: 'lb_pv1',
                  name: '礼包1',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv2',
                  name: '礼包2',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv3',
                  name: '礼包3',
                  value: '123/1232',
                },
              ],
            },
          ],
          headerMap: [
            {
              key: 'name',
              name: '抽奖名称(抽奖别名)',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'zht_pv',
              name: '参与抽奖人数/次数',
              desc: '参与定义:点击过抽奖按钮',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'zht_uv',
              name: '成功抽奖人数/次数',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'user_click_cnt',
              name: '发放抽奖机会数量',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'lb_pv',
              name: '礼包兑换人数/次数',
              isMergedKey: false,
              isMergedHeader: true,
            },
          ],
        });
      
      ...
      
      <Table rowKey="dt" dataSource={_dataSource} columns={columns} pagination={false} bordered />
      

      6.png

    • 动态合并表头的Table

      const { columns, _dataSource } = useTable({
          dataSource: [
            {
              key: 1,
              name: '抽奖1',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
              lb_pv: [
                {
                  key: 'lb_pv1',
                  name: '礼包1',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv2',
                  name: '礼包2',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv3',
                  name: '礼包3',
                  value: '123/1232',
                },
              ],
            },
            {
              key: 3,
              name: '抽奖2',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
              lb_pv: [
                {
                  key: 'lb_pv1',
                  name: '礼包1',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv2',
                  name: '礼包2',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv3',
                  name: '礼包3',
                  value: '123/1232',
                },
              ],
            },
            {
              key: 2,
              name: '抽奖3',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
              lb_pv: [
                {
                  key: 'lb_pv1',
                  name: '礼包1',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv2',
                  name: '礼包2',
                  value: '123/1232',
                },
                {
                  key: 'lb_pv3',
                  name: '礼包3',
                  value: '123/1232',
                },
              ],
            },
          ],
          headerMap: [
            {
              key: 'name',
              name: '抽奖名称(抽奖别名)',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'zht_pv',
              name: '参与抽奖人数/次数',
              desc: '参与定义:点击过抽奖按钮',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'zht_uv',
              name: '成功抽奖人数/次数',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'user_click_cnt',
              name: '发放抽奖机会数量',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'lb_pv',
              name: '礼包兑换人数/次数',
              isMergedKey: false,
              isMergedHeader: true,
            },
          ],
        });
      
      <Table rowKey="dt" dataSource={_dataSource} columns={columns} pagination={false} bordered />
      

      7.png

    • 动态合并表单项的Table

      const { columns, _dataSource } = useTable({
          dataSource: [
            {
              key: 1,
              name: '抽奖1',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
            },
            {
              key: 3,
              name: '抽奖1',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
            },
            {
              key: 2,
              name: '抽奖2',
              zht_pv: '1221/1234',
              zht_uv: '1221/1234',
              user_click_cnt: '1123',
            },
          ],
          headerMap: [
            {
              key: 'name',
              name: '抽奖名称(抽奖别名)',
              isMergedKey: true,
              isMergedHeader: false,
            },
            {
              key: 'zht_pv',
              name: '参与抽奖人数/次数',
              desc: '参与定义:点击过抽奖按钮',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'zht_uv',
              name: '成功抽奖人数/次数',
              isMergedKey: false,
              isMergedHeader: false,
            },
            {
              key: 'user_click_cnt',
              name: '发放抽奖机会数量',
              isMergedKey: true,
              isMergedHeader: false,
            },
          ],
        });
      
      <Table rowKey="dt" dataSource={_dataSource} columns={columns} pagination={false} bordered />
      

      8.png