antd table expandedRowRender踩坑

背景

在做某次需求时需要用到antd tableexpandableRowRender展示处理。在开发的时候mock数据显示是正常的。不出意外的话就要出意外了,获取数据是正常的,然后发现展开功能u没有展示。

我传给antd table的拓展配置大概是这样的,并没有调用expandedRowRender函数。

javascript 复制代码
const expandableParams = useMemo(
    () => ({
      // 是否默认展开所有行
      defaultExpandAllRows: true,
      expandIcon: () => null,
      expandRowByClick: false,
      expandedRowRender: (record: InfoResDataReturnGoodsList) => {
        // ...
        return <RefundDeail data={record} key={`RefundDeail${record.skuId}`} />
      },
      columnWidth: 0,
      // ...
    }),
    [],
  );

我又重新尝试了mock数据,改成同步传给table这个时候是能够正常显示的,就在猜想是不是数据异步的问题。开始研究问题所在~

开始正题

思路:因为不想研究他的整个table源码和懒惰 😭。换了个思路研究,首先让其正常显示,debug住调用expandedRowRender函数,看他的调用栈,然后找到调用的判断条件。

css 复制代码
if (rowSupportExpand && (expandRended || expanded)) {
    // 顺着调用栈 找到是在这调用的
    var expandContent = expandedRowRender(record, index, indent + 1, expanded);
    var computedExpandedRowClassName = expandedRowClassName && expandedRowClassName(record, index, indent);
    expandRowNode = /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_3__["createElement"](_ExpandedRow__WEBPACK_IMPORTED_MODULE_9__["default"], {
      expanded: expanded,
      className: classnames__WEBPACK_IMPORTED_MODULE_4___default()("".concat(prefixCls, "-expanded-row"), "".concat(prefixCls, "-expanded-row-level-").concat(indent + 1), computedExpandedRowClassName),
      prefixCls: prefixCls,
      component: RowComponent,
      cellComponent: cellComponent,
      colSpan: flattenColumns.length,
      isEmpty: false
    }, expandContent);
  }

为了方便查看点需要找到他的源码。因为我们debug的是webpack打包后的代码,应该会有指向源文件的信息。debug停住的地方是是归属于BodyRow函数的,试着在当前文件搜了下BodyRow,看有没有指向源文件信息。

文件路径是*/ "./node_modules/.pnpm/rc-table@7.26.0_react-dom@16.14.0+react@16.14.0/node_modules/rc-table/es/Body/BodyRow.js

下载rc-table关注BodyRow组件,在源码中找到上文debug停住的代码进行分析。以下都是伪代码分析,只保留了关键部分。

ini 复制代码
function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
  props: BodyRowProps<RecordType>,
) {
  const {
    className,
    style,
    record,
    index,
    renderIndex,
    rowKey,
    rowExpandable,
    expandedKeys,
    onRow,
    indent = 0,
    rowComponent: RowComponent,
    cellComponent,
    childrenColumnName,
  } = props;
  const {
    // ...
    expandableType,
    // ...
  } = React.useContext(BodyContext);
  // ...
  
  const [expandRended, setExpandRended] = React.useState(false);
​
  // 判断是否显示展开行的重要条件
  const expanded = expandedKeys && expandedKeys.has(props.recordKey);
​
  // 我感觉这里的逻辑是不必要的 下面的判断中是 expandRended || expanded
  // expandRended 取决于 expanded
  // 这两个变量我理解是一致的 不知道是不是漏掉了其他逻辑 没看明白这里为啥要这样写
  React.useEffect(() => {
    if (expanded) {
      setExpandRended(true);
    }
  }, [expanded]);
​
  // 判断是否显示展开行的重要条件
  // 这个我验证过了 rowExpandable 是我们配置中传的 没穿的话默认为true
  // 所以这里的条件主要是expandableType === 'row'
  const rowSupportExpand = expandableType === 'row' && (!rowExpandable || rowExpandable(record));
​
  // ...
​
  // ======================== Expand Row =========================
  // 展开行元素
  let expandRowNode: React.ReactElement;
  if (rowSupportExpand && (expandRended || expanded)) {
    const expandContent = expandedRowRender(record, index, indent + 1, expanded);
    const computedExpandedRowClassName =
      expandedRowClassName && expandedRowClassName(record, index, indent);
    expandRowNode = (
      <ExpandedRow
        expanded={expanded}
        className={classNames(
          `${prefixCls}-expanded-row`,
          `${prefixCls}-expanded-row-level-${indent + 1}`,
          computedExpandedRowClassName,
        )}
        prefixCls={prefixCls}
        component={RowComponent}
        cellComponent={cellComponent}
        colSpan={flattenColumns.length}
        isEmpty={false}
      >
        {expandContent}
      </ExpandedRow>
    );
  }
​
  return (
    <>
      {baseRowNode}
      {expandRowNode}
    </>
  );
}

从上面代码分析后可以看出是否显示展开行元素主要取决于expandableType === 'row'expandedKeys && expandedKeys.has(props.recordKey)。其中expandableType是从BodyContext取值,expandedKeys是父组件传进来的。

javascript 复制代码
// src/context/BodyContext.tsx
const BodyContext = React.createContext<BodyContextProps>(null);
​
export default BodyContext;

先找到BodyContext定义,本来想通过它的定义找到注入的地方的,但是引用的地方有四五处,不太好判断。因为context的逻辑取最近的一层,不确定是否有嵌套。所以从BodyRow的父组件开始

ini 复制代码
function Body<RecordType>({
  // ...
  getRowKey,
  measureColumnWidth,
  expandedKeys,
  // ...
}: BodyProps<RecordType>) {
​
  // ...
​
  // ====================== Render ======================
  const bodyNode = React.useMemo(() => {
    // ...
​
    let rows: React.ReactNode;
    if (data.length) {
      rows = flattenData.map((item, idx) => {
        const { record, indent, index: renderIndex } = item;
​
        const key = getRowKey(record, idx);
​
        // 主要关注传递expandedKeys  和 recordKey
        return (
          <BodyRow
            // ...
            rowKey={key}
            // ...
            expandedKeys={expandedKeys}
            // ...
          />
        );
      });
    } else {
      // ...
    }
​
    const columnsKey = getColumnsKey(flattenColumns);
​
    return (
      <WrapperComponent className={`${prefixCls}-tbody`}>
        {/* Measure body column width with additional hidden col */}
        {measureColumnWidth && (
          <MeasureRow
            prefixCls={prefixCls}
            columnsKey={columnsKey}
            onColumnResize={onColumnResize}
          />
        )}
​
        {rows}
      </WrapperComponent>
    );
  }, [
    data,
    prefixCls,
    onRow,
    measureColumnWidth,
    expandedKeys,
    getRowKey,
    getComponent,
    emptyNode,
    flattenColumns,
    childrenColumnName,
    onColumnResize,
    rowExpandable,
    flattenData,
  ]);
​
  return (
    <PerfContext.Provider value={perfRef.current}>
      <HoverContext.Provider value={{ startRow, endRow, onHover }}>
        {bodyNode}
      </HoverContext.Provider>
    </PerfContext.Provider>
  );
}

这里主要关注传递expandedKeysrecordKey

javascript 复制代码
function Table<RecordType extends DefaultRecordType>(props: TableProps<RecordType>) {
    // ...
    
    // 根据props生成拓展配置属性
    const expandableConfig = getExpandableProps(props);
​
    const {
      // ...
      // 需要展开的keys
      expandedRowKeys,
      // ...
    } = expandableConfig;
    
    // ...
    
    const mergedExpandedKeys = React.useMemo(
      () => new Set(expandedRowKeys || innerExpandedKeys || []),
      [expandedRowKeys, innerExpandedKeys],
    );
    
    // 根据传递进来的rowKey生成recordKey
    const getRowKey = React.useMemo<GetRowKey<RecordType>>(() => {
    if (typeof rowKey === 'function') {
      return rowKey;
    }
    return (record: RecordType) => {
      const key = record && record[rowKey];
​
      if (process.env.NODE_ENV !== 'production') {
        warning(
          key !== undefined,
          'Each record in table should have a unique `key` prop, or set `rowKey` to an unique primary key.',
        );
      }
​
      return key;
    };
  }, [rowKey]);
  
  // ...
  
  // 这里是引用`Body`组件
  // 主要关注 mergedExpandedKeys 和 getRowKey
  const bodyTable = (
    <Body
      data={mergedData}
      measureColumnWidth={fixHeader || horizonScroll || isSticky}
      expandedKeys={mergedExpandedKeys}
      rowExpandable={rowExpandable}
      getRowKey={getRowKey}
      onRow={onRow}
      emptyNode={emptyNode}
      childrenColumnName={mergedChildrenColumnName}
    />
  );
  
  // ...
  
  return (
    <StickyContext.Provider value={supportSticky}>
      <TableContext.Provider value={TableContextValue}>
        <BodyContext.Provider value={BodyContextValue}>
          <ExpandedRowContext.Provider value={ExpandedRowContextValue}>
            <ResizeContext.Provider value={ResizeContextValue}>{fullTable}</ResizeContext.Provider>
          </ExpandedRowContext.Provider>
        </BodyContext.Provider>
      </TableContext.Provider>
    </StickyContext.Provider>
  );
}

可以看到mergedExpandedKeys主要是根据expandedRowKeysinnerExpandedKeys生成的。而expandedRowKeys是我们自己配置的属性,getExpandableProps只是简单的检查了属性所以这里主要关注innerExpandedKeys。在这里也注意到BodyContext也就是上文说到的expandableType的取值。稍等下我们再来看看,先看看innerExpandedKeys,也先记住这里的getRowKey函数,等下会有作用。

kotlin 复制代码
function Table<RecordType extends DefaultRecordType>(
  props: TableProps<RecordType>
) {
  const {
    // ...
    // 这里的data是我们的源数据 只是经过了antd table的处理
    data,
    // ...
  } = props;
​
  // ...
  
  const mergedData = data || EMPTY_DATA;
​
  const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => {
    // 默认展开的keys
    if (defaultExpandedRowKeys) {
      return defaultExpandedRowKeys;
    }
    // 是否展开所有的所有行
    if (defaultExpandAllRows) {
      // 根据数据生成需要展开的行key
      return findAllChildrenKeys<RecordType>(
        mergedData,
        getRowKey,
        mergedChildrenColumnName
      );
    }
    return [];
  });
  
  // ...
}
  

这里注意到innerExpandedKeys是一个state变量。在初始组件的时候会生成一次默认值。我们传递了defaultExpandAllRows。它执行的逻辑应该是findAllChildrenKeys。找到findAllChildrenKeys的定义

ini 复制代码
export function findAllChildrenKeys<RecordType>(
  data: readonly RecordType[],
  // 这里就是上文让记住的生成key的函数
  getRowKey: GetRowKey<RecordType>,
  childrenColumnName: string,
): Key[] {
  const keys: Key[] = [];
​
  function dig(list: readonly RecordType[]) {
    (list || []).forEach((item, index) => {
      keys.push(getRowKey(item, index));
​
      dig((item as any)[childrenColumnName]);
    });
  }
​
  dig(data);
​
  return keys;
}

可以看到它就是根据传进来的data找到对应的rowKey生成需要展开的keys,再来看看更新的逻辑

ini 复制代码
const onTriggerExpand: TriggerEventHandler<RecordType> = React.useCallback(
    (record: RecordType) => {
      const key = getRowKey(record, mergedData.indexOf(record));

      let newExpandedKeys: Key[];
      const hasKey = mergedExpandedKeys.has(key);
      if (hasKey) {
        mergedExpandedKeys.delete(key);
        newExpandedKeys = [...mergedExpandedKeys];
      } else {
        newExpandedKeys = [...mergedExpandedKeys, key];
      }

			// 更新
      setInnerExpandedKeys(newExpandedKeys);
      if (onExpand) {
        onExpand(!hasKey, record);
      }
      if (onExpandedRowsChange) {
        onExpandedRowsChange(newExpandedKeys);
      }
    },
    [getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange],
  );

在当前上文中只找到了一处更新innerExpandedKeys的逻辑,看到这里大概也就明白了🤔️innerExpandedKeystable初始时才会执行一遍这里的逻辑。但是数据又是异步的,所以innerExpandedKeys始终是空数组。那么上文说到判断是否展开的const expanded = expandedKeys && expandedKeys.has(props.recordKey);这段逻辑就永为false了。感兴趣的同学可以接着往下看expandableType,我反手直接跳到总结🐶。

typescript 复制代码
// ...
const expandableType = React.useMemo<ExpandableType>(() => {
		// 这里就是我们传进来的配置属性
    if (expandedRowRender) {
      return 'row';
    }
    /* eslint-disable no-underscore-dangle */
    /**
     * Fix https://github.com/ant-design/ant-design/issues/21154
     * This is a workaround to not to break current behavior.
     * We can remove follow code after final release.
     *
     * To other developer:
     *  Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor
     */
    if (
      (props.expandable &&
        internalHooks === INTERNAL_HOOKS &&
        (props.expandable as any).__PARENT_RENDER_ICON__) ||
      mergedData.some(
        record => record && typeof record === 'object' && record[mergedChildrenColumnName],
      )
    ) {
      return 'nest';
    }
    /* eslint-enable */
    return false;
  }, [!!expandedRowRender, mergedData]);

// ...
const BodyContextValue = React.useMemo(
    () => ({
      // ...
      expandableType,
      // ...
    }),
    [
      columnContext,
      mergedTableLayout,
      rowClassName,
      expandedRowClassName,
      mergedExpandIcon,
      expandableType,
      expandRowByClick,
      expandedRowRender,
      onTriggerExpand,
      expandIconColumnIndex,
      indentSize,
    ],
  );
  
  // ...

可以看到expandableType会为row值。

总结

  1. 当我们传递了默认展开所有行,但数据是异步的时候不会展示展开行,主要是因为innerExpandedKeys。他只会在组件初始化的时候去生成key。当数据异步的时候就有问题了。
  2. 我们可以通过手动配置expandedRowKeys解决这个问题🐶
  3. github.com/ant-design/... 在这找到了类似的问题。但是好像解释说default is one time operation which only sync when component mounted.。默认就是组件挂载时的一次性行为,好像设计理念如此,有点疑惑🤔️为啥要这样设计嘞,默认展开所有行不能直接让所有行都展开吗。还要去走一遍生成keys的逻辑
相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui