一. 自定义hooks-驱动条件
hooks本质上是一个函数。函数的执行,决定与无状态组件组件自身的执行上下文。每次函数的执行(本质上就是组件的更新)就会执行自定义hooks的执行,由此可见组件本身执行和hooks的执行如出一辙。
那么prop的修改,useState,useReducer使用是无状态组件更新条件,那么就是驱动hooks执行的条件。我们用一幅图来表示如上关系。
二. 自定义hooks-通用模式
我们设计的自定义react-hooks应该是长的这样的。
const [ xxx , ... ] = useXXX(参数A,参数B...)
在我们在编写自定义hooks的时候,要特别~特别~特别关注的是传进去什么,返回什么。返回的东西是我们真正需要的。更像一个工厂,把原材料加工,最后返回我们。正如上图所示
三. 自定义hooks实战
1. useLineEchart
-
遇到的问题:
- 【活动详情】页面中存在大量相同的 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. useTable
-
遇到的问题:
-
antd table 组件动态表头合并
-
- *antd table 组件动态表单项合并*
- *antd table 组件表头 hover 状态,显示表头说明*
-
解决方法:
统一将接口数据转换成新的 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 /> -
动态合并表头的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 /> -
动态合并表单项的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 />
-