Browse Source

refactor: Table render logic opt (#40063)

* chore: basic prepare

* chore: adjust render logic

* chore: fix lint

* chore: bump table

* test: update snapshot
pull/40178/head
二货爱吃白萝卜 2 years ago
committed by GitHub
parent
commit
91df688efd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 553
      components/table/InternalTable.tsx
  2. 12
      components/table/RcTable.tsx
  3. 579
      components/table/Table.tsx
  4. 2265
      components/table/__tests__/__snapshots__/demo-extend.test.ts.snap
  5. 2265
      components/table/__tests__/__snapshots__/demo.test.ts.snap
  6. 2
      components/table/__tests__/type.test.tsx
  7. 7
      components/table/demo/row-selection-debug.md
  8. 88
      components/table/demo/row-selection-debug.tsx
  9. 1
      components/table/index.en-US.md
  10. 3
      components/table/index.ts
  11. 1
      components/table/index.zh-CN.md
  12. 11
      components/table/interface.ts
  13. 2
      package.json

553
components/table/InternalTable.tsx

@ -0,0 +1,553 @@
import classNames from 'classnames';
import { convertChildrenToColumns } from 'rc-table/lib/hooks/useColumns';
import type { TableProps as RcTableProps } from 'rc-table/lib/Table';
import { INTERNAL_HOOKS } from 'rc-table/lib/Table';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import type { ConfigConsumerProps } from '../config-provider/context';
import { ConfigContext } from '../config-provider/context';
import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import useBreakpoint from '../grid/hooks/useBreakpoint';
import defaultLocale from '../locale/en_US';
import Pagination from '../pagination';
import type { SpinProps } from '../spin';
import Spin from '../spin';
import type { TooltipProps } from '../tooltip';
import type { Breakpoint } from '../_util/responsiveObserver';
import scrollTo from '../_util/scrollTo';
import warning from '../_util/warning';
import renderExpandIcon from './ExpandIcon';
import type { FilterState } from './hooks/useFilter';
import useFilter, { getFilterData } from './hooks/useFilter';
import useLazyKVMap from './hooks/useLazyKVMap';
import usePagination, { DEFAULT_PAGE_SIZE, getPaginationParam } from './hooks/usePagination';
import useSelection from './hooks/useSelection';
import type { SortState } from './hooks/useSorter';
import useSorter, { getSortData } from './hooks/useSorter';
import useTitleColumns from './hooks/useTitleColumns';
import type {
ColumnTitleProps,
ColumnType,
ExpandableConfig,
ExpandType,
FilterValue,
GetPopupContainer,
GetRowKey,
SorterResult,
SortOrder,
TableAction,
TableCurrentDataSource,
TableLocale,
TableRowSelection,
ColumnsType,
TablePaginationConfig,
RefInternalTable,
} from './interface';
import RcTable from './RcTable';
import useStyle from './style';
export type { ColumnsType, TablePaginationConfig };
const EMPTY_LIST: any[] = [];
interface ChangeEventInfo<RecordType> {
pagination: {
current?: number;
pageSize?: number;
total?: number;
};
filters: Record<string, FilterValue | null>;
sorter: SorterResult<RecordType> | SorterResult<RecordType>[];
filterStates: FilterState<RecordType>[];
sorterStates: SortState<RecordType>[];
resetPagination: Function;
}
/** Same as `TableProps` but we need record parent render times */
export interface InternalTableProps<RecordType> extends TableProps<RecordType> {
_renderTimes: number;
}
export interface TableProps<RecordType>
extends Omit<
RcTableProps<RecordType>,
| 'transformColumns'
| 'internalHooks'
| 'internalRefs'
| 'data'
| 'columns'
| 'scroll'
| 'emptyText'
> {
dropdownPrefixCls?: string;
dataSource?: RcTableProps<RecordType>['data'];
columns?: ColumnsType<RecordType>;
pagination?: false | TablePaginationConfig;
loading?: boolean | SpinProps;
size?: SizeType;
bordered?: boolean;
locale?: TableLocale;
onChange?: (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
extra: TableCurrentDataSource<RecordType>,
) => void;
rowSelection?: TableRowSelection<RecordType>;
getPopupContainer?: GetPopupContainer;
scroll?: RcTableProps<RecordType>['scroll'] & {
scrollToFirstRowOnChange?: boolean;
};
sortDirections?: SortOrder[];
showSorterTooltip?: boolean | TooltipProps;
}
function InternalTable<RecordType extends object = any>(
props: InternalTableProps<RecordType>,
ref: React.MutableRefObject<HTMLDivElement>,
) {
const {
prefixCls: customizePrefixCls,
className,
style,
size: customizeSize,
bordered,
dropdownPrefixCls: customizeDropdownPrefixCls,
dataSource,
pagination,
rowSelection,
rowKey = 'key',
rowClassName,
columns,
children,
childrenColumnName: legacyChildrenColumnName,
onChange,
getPopupContainer,
loading,
expandIcon,
expandable,
expandedRowRender,
expandIconColumnIndex,
indentSize,
scroll,
sortDirections,
locale,
showSorterTooltip = true,
} = props;
if (process.env.NODE_ENV !== 'production') {
warning(
!(typeof rowKey === 'function' && rowKey.length > 1),
'Table',
'`index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
);
}
const baseColumns = React.useMemo(
() => columns || (convertChildrenToColumns(children) as ColumnsType<RecordType>),
[columns, children],
);
const needResponsive = React.useMemo(
() => baseColumns.some((col: ColumnType<RecordType>) => col.responsive),
[baseColumns],
);
const screens = useBreakpoint(needResponsive);
const mergedColumns = React.useMemo(() => {
const matched = new Set(Object.keys(screens).filter((m: Breakpoint) => screens[m]));
return baseColumns.filter(
(c) => !c.responsive || c.responsive.some((r: Breakpoint) => matched.has(r)),
);
}, [baseColumns, screens]);
const tableProps: TableProps<RecordType> = omit(props, ['className', 'style', 'columns']);
const size = React.useContext<SizeType>(SizeContext);
const {
locale: contextLocale = defaultLocale,
direction,
renderEmpty,
getPrefixCls,
} = React.useContext<ConfigConsumerProps>(ConfigContext);
const mergedSize = customizeSize || size;
const tableLocale: TableLocale = { ...contextLocale.Table, ...locale };
const rawData: readonly RecordType[] = dataSource || EMPTY_LIST;
const prefixCls = getPrefixCls('table', customizePrefixCls);
const dropdownPrefixCls = getPrefixCls('dropdown', customizeDropdownPrefixCls);
const mergedExpandable: ExpandableConfig<RecordType> = {
childrenColumnName: legacyChildrenColumnName,
expandIconColumnIndex,
...expandable,
};
const { childrenColumnName = 'children' } = mergedExpandable;
const expandType = React.useMemo<ExpandType>(() => {
if (rawData.some((item) => (item as any)?.[childrenColumnName])) {
return 'nest';
}
if (expandedRowRender || (expandable && expandable.expandedRowRender)) {
return 'row';
}
return null;
}, [rawData]);
const internalRefs = {
body: React.useRef<HTMLDivElement>(),
};
// ============================ RowKey ============================
const getRowKey = React.useMemo<GetRowKey<RecordType>>(() => {
if (typeof rowKey === 'function') {
return rowKey;
}
return (record: RecordType) => (record as any)?.[rowKey as string];
}, [rowKey]);
const [getRecordByKey] = useLazyKVMap(rawData, childrenColumnName, getRowKey);
// ============================ Events =============================
const changeEventInfo: Partial<ChangeEventInfo<RecordType>> = {};
const triggerOnChange = (
info: Partial<ChangeEventInfo<RecordType>>,
action: TableAction,
reset: boolean = false,
) => {
const changeInfo = {
...changeEventInfo,
...info,
};
if (reset) {
changeEventInfo.resetPagination!();
// Reset event param
if (changeInfo.pagination!.current) {
changeInfo.pagination!.current = 1;
}
// Trigger pagination events
if (pagination && pagination.onChange) {
pagination.onChange(1, changeInfo.pagination!.pageSize!);
}
}
if (scroll && scroll.scrollToFirstRowOnChange !== false && internalRefs.body.current) {
scrollTo(0, {
getContainer: () => internalRefs.body.current!,
});
}
onChange?.(changeInfo.pagination!, changeInfo.filters!, changeInfo.sorter!, {
currentDataSource: getFilterData(
getSortData(rawData, changeInfo.sorterStates!, childrenColumnName),
changeInfo.filterStates!,
),
action,
});
};
/**
* Controlled state in `columns` is not a good idea that makes too many code (1000+ line?) to read
* state out and then put it back to title render. Move these code into `hooks` but still too
* complex. We should provides Table props like `sorter` & `filter` to handle control in next big
* version.
*/
// ============================ Sorter =============================
const onSorterChange = (
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
sorterStates: SortState<RecordType>[],
) => {
triggerOnChange(
{
sorter,
sorterStates,
},
'sort',
false,
);
};
const [transformSorterColumns, sortStates, sorterTitleProps, getSorters] = useSorter<RecordType>({
prefixCls,
mergedColumns,
onSorterChange,
sortDirections: sortDirections || ['ascend', 'descend'],
tableLocale,
showSorterTooltip,
});
const sortedData = React.useMemo(
() => getSortData(rawData, sortStates, childrenColumnName),
[rawData, sortStates],
);
changeEventInfo.sorter = getSorters();
changeEventInfo.sorterStates = sortStates;
// ============================ Filter ============================
const onFilterChange = (
filters: Record<string, FilterValue>,
filterStates: FilterState<RecordType>[],
) => {
triggerOnChange(
{
filters,
filterStates,
},
'filter',
true,
);
};
const [transformFilterColumns, filterStates, filters] = useFilter<RecordType>({
prefixCls,
locale: tableLocale,
dropdownPrefixCls,
mergedColumns,
onFilterChange,
getPopupContainer,
});
const mergedData = getFilterData(sortedData, filterStates);
changeEventInfo.filters = filters;
changeEventInfo.filterStates = filterStates;
// ============================ Column ============================
const columnTitleProps = React.useMemo<ColumnTitleProps<RecordType>>(() => {
const mergedFilters: Record<string, FilterValue> = {};
Object.keys(filters).forEach((filterKey) => {
if (filters[filterKey] !== null) {
mergedFilters[filterKey] = filters[filterKey]!;
}
});
return {
...sorterTitleProps,
filters: mergedFilters,
};
}, [sorterTitleProps, filters]);
const [transformTitleColumns] = useTitleColumns(columnTitleProps);
// ========================== Pagination ==========================
const onPaginationChange = (current: number, pageSize: number) => {
triggerOnChange(
{
pagination: { ...changeEventInfo.pagination, current, pageSize },
},
'paginate',
);
};
const [mergedPagination, resetPagination] = usePagination(
mergedData.length,
pagination,
onPaginationChange,
);
changeEventInfo.pagination =
pagination === false ? {} : getPaginationParam(pagination, mergedPagination);
changeEventInfo.resetPagination = resetPagination;
// ============================= Data =============================
const pageData = React.useMemo<RecordType[]>(() => {
if (pagination === false || !mergedPagination.pageSize) {
return mergedData;
}
const { current = 1, total, pageSize = DEFAULT_PAGE_SIZE } = mergedPagination;
warning(current > 0, 'Table', '`current` should be positive number.');
// Dynamic table data
if (mergedData.length < total!) {
if (mergedData.length > pageSize) {
warning(
false,
'Table',
'`dataSource` length is less than `pagination.total` but large than `pagination.pageSize`. Please make sure your config correct data with async mode.',
);
return mergedData.slice((current - 1) * pageSize, current * pageSize);
}
return mergedData;
}
return mergedData.slice((current - 1) * pageSize, current * pageSize);
}, [
!!pagination,
mergedData,
mergedPagination && mergedPagination.current,
mergedPagination && mergedPagination.pageSize,
mergedPagination && mergedPagination.total,
]);
// ========================== Selections ==========================
const [transformSelectionColumns, selectedKeySet] = useSelection<RecordType>(rowSelection, {
prefixCls,
data: mergedData,
pageData,
getRowKey,
getRecordByKey,
expandType,
childrenColumnName,
locale: tableLocale,
getPopupContainer,
});
const internalRowClassName = (record: RecordType, index: number, indent: number) => {
let mergedRowClassName: string;
if (typeof rowClassName === 'function') {
mergedRowClassName = classNames(rowClassName(record, index, indent));
} else {
mergedRowClassName = classNames(rowClassName);
}
return classNames(
{
[`${prefixCls}-row-selected`]: selectedKeySet.has(getRowKey(record, index)),
},
mergedRowClassName,
);
};
// ========================== Expandable ==========================
// Pass origin render status into `rc-table`, this can be removed when refactor with `rc-table`
(mergedExpandable as any).__PARENT_RENDER_ICON__ = mergedExpandable.expandIcon;
// Customize expandable icon
mergedExpandable.expandIcon =
mergedExpandable.expandIcon || expandIcon || renderExpandIcon(tableLocale!);
// Adjust expand icon index, no overwrite expandIconColumnIndex if set.
if (expandType === 'nest' && mergedExpandable.expandIconColumnIndex === undefined) {
mergedExpandable.expandIconColumnIndex = rowSelection ? 1 : 0;
} else if (mergedExpandable.expandIconColumnIndex! > 0 && rowSelection) {
mergedExpandable.expandIconColumnIndex! -= 1;
}
// Indent size
if (typeof mergedExpandable.indentSize !== 'number') {
mergedExpandable.indentSize = typeof indentSize === 'number' ? indentSize : 15;
}
// ============================ Render ============================
const transformColumns = React.useCallback(
(innerColumns: ColumnsType<RecordType>): ColumnsType<RecordType> =>
transformTitleColumns(
transformSelectionColumns(transformFilterColumns(transformSorterColumns(innerColumns))),
),
[transformSorterColumns, transformFilterColumns, transformSelectionColumns],
);
let topPaginationNode: React.ReactNode;
let bottomPaginationNode: React.ReactNode;
if (pagination !== false && mergedPagination?.total) {
let paginationSize: TablePaginationConfig['size'];
if (mergedPagination.size) {
paginationSize = mergedPagination.size;
} else {
paginationSize = mergedSize === 'small' || mergedSize === 'middle' ? 'small' : undefined;
}
const renderPagination = (position: string) => (
<Pagination
{...mergedPagination}
className={classNames(
`${prefixCls}-pagination ${prefixCls}-pagination-${position}`,
mergedPagination.className,
)}
size={paginationSize}
/>
);
const defaultPosition = direction === 'rtl' ? 'left' : 'right';
const { position } = mergedPagination;
if (position !== null && Array.isArray(position)) {
const topPos = position.find((p) => p.includes('top'));
const bottomPos = position.find((p) => p.includes('bottom'));
const isDisable = position.every((p) => `${p}` === 'none');
if (!topPos && !bottomPos && !isDisable) {
bottomPaginationNode = renderPagination(defaultPosition);
}
if (topPos) {
topPaginationNode = renderPagination(topPos!.toLowerCase().replace('top', ''));
}
if (bottomPos) {
bottomPaginationNode = renderPagination(bottomPos!.toLowerCase().replace('bottom', ''));
}
} else {
bottomPaginationNode = renderPagination(defaultPosition);
}
}
// >>>>>>>>> Spinning
let spinProps: SpinProps | undefined;
if (typeof loading === 'boolean') {
spinProps = {
spinning: loading,
};
} else if (typeof loading === 'object') {
spinProps = {
spinning: true,
...loading,
};
}
// Style
const [wrapSSR, hashId] = useStyle(prefixCls);
const wrapperClassNames = classNames(
`${prefixCls}-wrapper`,
{
[`${prefixCls}-wrapper-rtl`]: direction === 'rtl',
},
className,
hashId,
);
return wrapSSR(
<div ref={ref} className={wrapperClassNames} style={style}>
<Spin spinning={false} {...spinProps}>
{topPaginationNode}
<RcTable<RecordType>
{...tableProps}
columns={mergedColumns as RcTableProps<RecordType>['columns']}
direction={direction}
expandable={mergedExpandable}
prefixCls={prefixCls}
className={classNames({
[`${prefixCls}-middle`]: mergedSize === 'middle',
[`${prefixCls}-small`]: mergedSize === 'small',
[`${prefixCls}-bordered`]: bordered,
[`${prefixCls}-empty`]: rawData.length === 0,
})}
data={pageData}
rowKey={getRowKey}
rowClassName={internalRowClassName}
emptyText={(locale && locale.emptyText) || (renderEmpty || defaultRenderEmpty)('Table')}
// Internal
internalHooks={INTERNAL_HOOKS}
internalRefs={internalRefs as any}
transformColumns={transformColumns as RcTableProps<RecordType>['transformColumns']}
/>
{bottomPaginationNode}
</Spin>
</div>,
);
}
export default React.forwardRef(InternalTable) as RefInternalTable;

12
components/table/RcTable.tsx

@ -0,0 +1,12 @@
import { genTable } from 'rc-table';
import type { InternalTableProps } from './InternalTable';
/**
* Same as `rc-table` but we modify trigger children update logic instead.
*/
export default genTable((prev, next) => {
const { _renderTimes: prevRenderTimes } = prev as InternalTableProps<any>;
const { _renderTimes: nextRenderTimes } = next as InternalTableProps<any>;
return prevRenderTimes !== nextRenderTimes;
});

579
components/table/Table.tsx

@ -1,565 +1,30 @@
import classNames from 'classnames';
import RcTable, { Summary } from 'rc-table';
import { convertChildrenToColumns } from 'rc-table/lib/hooks/useColumns';
import type { TableProps as RcTableProps } from 'rc-table/lib/Table';
import { INTERNAL_HOOKS } from 'rc-table/lib/Table';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import type { ConfigConsumerProps } from '../config-provider/context';
import { ConfigContext } from '../config-provider/context';
import defaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import type { SizeType } from '../config-provider/SizeContext';
import SizeContext from '../config-provider/SizeContext';
import useBreakpoint from '../grid/hooks/useBreakpoint';
import defaultLocale from '../locale/en_US';
import Pagination from '../pagination';
import type { SpinProps } from '../spin';
import Spin from '../spin';
import type { TooltipProps } from '../tooltip';
import type { Breakpoint } from '../_util/responsiveObserver';
import scrollTo from '../_util/scrollTo';
import warning from '../_util/warning';
import { Summary, EXPAND_COLUMN } from 'rc-table';
import Column from './Column';
import ColumnGroup from './ColumnGroup';
import renderExpandIcon from './ExpandIcon';
import type { FilterState } from './hooks/useFilter';
import useFilter, { getFilterData } from './hooks/useFilter';
import useLazyKVMap from './hooks/useLazyKVMap';
import usePagination, { DEFAULT_PAGE_SIZE, getPaginationParam } from './hooks/usePagination';
import useSelection, {
import InternalTable from './InternalTable';
import type { TableProps } from './InternalTable';
import type { RefTable } from './interface';
import {
SELECTION_ALL,
SELECTION_COLUMN,
SELECTION_INVERT,
SELECTION_NONE,
} from './hooks/useSelection';
import type { SortState } from './hooks/useSorter';
import useSorter, { getSortData } from './hooks/useSorter';
import useTitleColumns from './hooks/useTitleColumns';
import type {
ColumnTitleProps,
ColumnType,
ExpandableConfig,
ExpandType,
FilterValue,
GetPopupContainer,
GetRowKey,
SorterResult,
SortOrder,
TableAction,
TableCurrentDataSource,
TableLocale,
TableRowSelection,
ColumnsType,
TablePaginationConfig,
} from './interface';
import useStyle from './style';
export type { ColumnsType, TablePaginationConfig };
const EMPTY_LIST: any[] = [];
interface ChangeEventInfo<RecordType> {
pagination: {
current?: number;
pageSize?: number;
total?: number;
};
filters: Record<string, FilterValue | null>;
sorter: SorterResult<RecordType> | SorterResult<RecordType>[];
filterStates: FilterState<RecordType>[];
sorterStates: SortState<RecordType>[];
resetPagination: Function;
}
export interface TableProps<RecordType>
extends Omit<
RcTableProps<RecordType>,
| 'transformColumns'
| 'internalHooks'
| 'internalRefs'
| 'data'
| 'columns'
| 'scroll'
| 'emptyText'
> {
dropdownPrefixCls?: string;
dataSource?: RcTableProps<RecordType>['data'];
columns?: ColumnsType<RecordType>;
pagination?: false | TablePaginationConfig;
loading?: boolean | SpinProps;
size?: SizeType;
bordered?: boolean;
locale?: TableLocale;
onChange?: (
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
extra: TableCurrentDataSource<RecordType>,
) => void;
rowSelection?: TableRowSelection<RecordType>;
getPopupContainer?: GetPopupContainer;
scroll?: RcTableProps<RecordType>['scroll'] & {
scrollToFirstRowOnChange?: boolean;
};
sortDirections?: SortOrder[];
showSorterTooltip?: boolean | TooltipProps;
}
function InternalTable<RecordType extends object = any>(
function Table<RecordType extends object = any>(
props: TableProps<RecordType>,
ref: React.MutableRefObject<HTMLDivElement>,
ref: React.Ref<HTMLDivElement>,
) {
const {
prefixCls: customizePrefixCls,
className,
style,
size: customizeSize,
bordered,
dropdownPrefixCls: customizeDropdownPrefixCls,
dataSource,
pagination,
rowSelection,
rowKey = 'key',
rowClassName,
columns,
children,
childrenColumnName: legacyChildrenColumnName,
onChange,
getPopupContainer,
loading,
expandIcon,
expandable,
expandedRowRender,
expandIconColumnIndex,
indentSize,
scroll,
sortDirections,
locale,
showSorterTooltip = true,
} = props;
if (process.env.NODE_ENV !== 'production') {
warning(
!(typeof rowKey === 'function' && rowKey.length > 1),
'Table',
'`index` parameter of `rowKey` function is deprecated. There is no guarantee that it will work as expected.',
);
}
const baseColumns = React.useMemo(
() => columns || (convertChildrenToColumns(children) as ColumnsType<RecordType>),
[columns, children],
);
const needResponsive = React.useMemo(
() => baseColumns.some((col: ColumnType<RecordType>) => col.responsive),
[baseColumns],
);
const screens = useBreakpoint(needResponsive);
const mergedColumns = React.useMemo(() => {
const matched = new Set(Object.keys(screens).filter((m: Breakpoint) => screens[m]));
return baseColumns.filter(
(c) => !c.responsive || c.responsive.some((r: Breakpoint) => matched.has(r)),
);
}, [baseColumns, screens]);
const tableProps: TableProps<RecordType> = omit(props, ['className', 'style', 'columns']);
const size = React.useContext<SizeType>(SizeContext);
const {
locale: contextLocale = defaultLocale,
direction,
renderEmpty,
getPrefixCls,
} = React.useContext<ConfigConsumerProps>(ConfigContext);
const mergedSize = customizeSize || size;
const tableLocale: TableLocale = { ...contextLocale.Table, ...locale };
const rawData: readonly RecordType[] = dataSource || EMPTY_LIST;
const prefixCls = getPrefixCls('table', customizePrefixCls);
const dropdownPrefixCls = getPrefixCls('dropdown', customizeDropdownPrefixCls);
const mergedExpandable: ExpandableConfig<RecordType> = {
childrenColumnName: legacyChildrenColumnName,
expandIconColumnIndex,
...expandable,
};
const { childrenColumnName = 'children' } = mergedExpandable;
const expandType = React.useMemo<ExpandType>(() => {
if (rawData.some((item) => (item as any)?.[childrenColumnName])) {
return 'nest';
}
if (expandedRowRender || (expandable && expandable.expandedRowRender)) {
return 'row';
}
return null;
}, [rawData]);
const internalRefs = {
body: React.useRef<HTMLDivElement>(),
};
// ============================ RowKey ============================
const getRowKey = React.useMemo<GetRowKey<RecordType>>(() => {
if (typeof rowKey === 'function') {
return rowKey;
}
return (record: RecordType) => (record as any)?.[rowKey as string];
}, [rowKey]);
const [getRecordByKey] = useLazyKVMap(rawData, childrenColumnName, getRowKey);
// ============================ Events =============================
const changeEventInfo: Partial<ChangeEventInfo<RecordType>> = {};
const triggerOnChange = (
info: Partial<ChangeEventInfo<RecordType>>,
action: TableAction,
reset: boolean = false,
) => {
const changeInfo = {
...changeEventInfo,
...info,
};
if (reset) {
changeEventInfo.resetPagination!();
// Reset event param
if (changeInfo.pagination!.current) {
changeInfo.pagination!.current = 1;
}
// Trigger pagination events
if (pagination && pagination.onChange) {
pagination.onChange(1, changeInfo.pagination!.pageSize!);
}
}
const renderTimesRef = React.useRef(0);
renderTimesRef.current += 1;
if (scroll && scroll.scrollToFirstRowOnChange !== false && internalRefs.body.current) {
scrollTo(0, {
getContainer: () => internalRefs.body.current!,
});
}
onChange?.(changeInfo.pagination!, changeInfo.filters!, changeInfo.sorter!, {
currentDataSource: getFilterData(
getSortData(rawData, changeInfo.sorterStates!, childrenColumnName),
changeInfo.filterStates!,
),
action,
});
};
/**
* Controlled state in `columns` is not a good idea that makes too many code (1000+ line?) to read
* state out and then put it back to title render. Move these code into `hooks` but still too
* complex. We should provides Table props like `sorter` & `filter` to handle control in next big
* version.
*/
// ============================ Sorter =============================
const onSorterChange = (
sorter: SorterResult<RecordType> | SorterResult<RecordType>[],
sorterStates: SortState<RecordType>[],
) => {
triggerOnChange(
{
sorter,
sorterStates,
},
'sort',
false,
);
};
const [transformSorterColumns, sortStates, sorterTitleProps, getSorters] = useSorter<RecordType>({
prefixCls,
mergedColumns,
onSorterChange,
sortDirections: sortDirections || ['ascend', 'descend'],
tableLocale,
showSorterTooltip,
});
const sortedData = React.useMemo(
() => getSortData(rawData, sortStates, childrenColumnName),
[rawData, sortStates],
);
changeEventInfo.sorter = getSorters();
changeEventInfo.sorterStates = sortStates;
// ============================ Filter ============================
const onFilterChange = (
filters: Record<string, FilterValue>,
filterStates: FilterState<RecordType>[],
) => {
triggerOnChange(
{
filters,
filterStates,
},
'filter',
true,
);
};
const [transformFilterColumns, filterStates, filters] = useFilter<RecordType>({
prefixCls,
locale: tableLocale,
dropdownPrefixCls,
mergedColumns,
onFilterChange,
getPopupContainer,
});
const mergedData = getFilterData(sortedData, filterStates);
changeEventInfo.filters = filters;
changeEventInfo.filterStates = filterStates;
// ============================ Column ============================
const columnTitleProps = React.useMemo<ColumnTitleProps<RecordType>>(() => {
const mergedFilters: Record<string, FilterValue> = {};
Object.keys(filters).forEach((filterKey) => {
if (filters[filterKey] !== null) {
mergedFilters[filterKey] = filters[filterKey]!;
}
});
return {
...sorterTitleProps,
filters: mergedFilters,
};
}, [sorterTitleProps, filters]);
const [transformTitleColumns] = useTitleColumns(columnTitleProps);
// ========================== Pagination ==========================
const onPaginationChange = (current: number, pageSize: number) => {
triggerOnChange(
{
pagination: { ...changeEventInfo.pagination, current, pageSize },
},
'paginate',
);
};
const [mergedPagination, resetPagination] = usePagination(
mergedData.length,
pagination,
onPaginationChange,
);
changeEventInfo.pagination =
pagination === false ? {} : getPaginationParam(pagination, mergedPagination);
changeEventInfo.resetPagination = resetPagination;
// ============================= Data =============================
const pageData = React.useMemo<RecordType[]>(() => {
if (pagination === false || !mergedPagination.pageSize) {
return mergedData;
}
const { current = 1, total, pageSize = DEFAULT_PAGE_SIZE } = mergedPagination;
warning(current > 0, 'Table', '`current` should be positive number.');
// Dynamic table data
if (mergedData.length < total!) {
if (mergedData.length > pageSize) {
warning(
false,
'Table',
'`dataSource` length is less than `pagination.total` but large than `pagination.pageSize`. Please make sure your config correct data with async mode.',
);
return mergedData.slice((current - 1) * pageSize, current * pageSize);
}
return mergedData;
}
return mergedData.slice((current - 1) * pageSize, current * pageSize);
}, [
!!pagination,
mergedData,
mergedPagination && mergedPagination.current,
mergedPagination && mergedPagination.pageSize,
mergedPagination && mergedPagination.total,
]);
// ========================== Selections ==========================
const [transformSelectionColumns, selectedKeySet] = useSelection<RecordType>(rowSelection, {
prefixCls,
data: mergedData,
pageData,
getRowKey,
getRecordByKey,
expandType,
childrenColumnName,
locale: tableLocale,
getPopupContainer,
});
const internalRowClassName = (record: RecordType, index: number, indent: number) => {
let mergedRowClassName: string;
if (typeof rowClassName === 'function') {
mergedRowClassName = classNames(rowClassName(record, index, indent));
} else {
mergedRowClassName = classNames(rowClassName);
}
return classNames(
{
[`${prefixCls}-row-selected`]: selectedKeySet.has(getRowKey(record, index)),
},
mergedRowClassName,
);
};
// ========================== Expandable ==========================
// Pass origin render status into `rc-table`, this can be removed when refactor with `rc-table`
(mergedExpandable as any).__PARENT_RENDER_ICON__ = mergedExpandable.expandIcon;
// Customize expandable icon
mergedExpandable.expandIcon =
mergedExpandable.expandIcon || expandIcon || renderExpandIcon(tableLocale!);
// Adjust expand icon index, no overwrite expandIconColumnIndex if set.
if (expandType === 'nest' && mergedExpandable.expandIconColumnIndex === undefined) {
mergedExpandable.expandIconColumnIndex = rowSelection ? 1 : 0;
} else if (mergedExpandable.expandIconColumnIndex! > 0 && rowSelection) {
mergedExpandable.expandIconColumnIndex! -= 1;
}
// Indent size
if (typeof mergedExpandable.indentSize !== 'number') {
mergedExpandable.indentSize = typeof indentSize === 'number' ? indentSize : 15;
}
// ============================ Render ============================
const transformColumns = React.useCallback(
(innerColumns: ColumnsType<RecordType>): ColumnsType<RecordType> =>
transformTitleColumns(
transformSelectionColumns(transformFilterColumns(transformSorterColumns(innerColumns))),
),
[transformSorterColumns, transformFilterColumns, transformSelectionColumns],
);
let topPaginationNode: React.ReactNode;
let bottomPaginationNode: React.ReactNode;
if (pagination !== false && mergedPagination?.total) {
let paginationSize: TablePaginationConfig['size'];
if (mergedPagination.size) {
paginationSize = mergedPagination.size;
} else {
paginationSize = mergedSize === 'small' || mergedSize === 'middle' ? 'small' : undefined;
}
const renderPagination = (position: string) => (
<Pagination
{...mergedPagination}
className={classNames(
`${prefixCls}-pagination ${prefixCls}-pagination-${position}`,
mergedPagination.className,
)}
size={paginationSize}
/>
);
const defaultPosition = direction === 'rtl' ? 'left' : 'right';
const { position } = mergedPagination;
if (position !== null && Array.isArray(position)) {
const topPos = position.find((p) => p.includes('top'));
const bottomPos = position.find((p) => p.includes('bottom'));
const isDisable = position.every((p) => `${p}` === 'none');
if (!topPos && !bottomPos && !isDisable) {
bottomPaginationNode = renderPagination(defaultPosition);
}
if (topPos) {
topPaginationNode = renderPagination(topPos!.toLowerCase().replace('top', ''));
}
if (bottomPos) {
bottomPaginationNode = renderPagination(bottomPos!.toLowerCase().replace('bottom', ''));
}
} else {
bottomPaginationNode = renderPagination(defaultPosition);
}
}
// >>>>>>>>> Spinning
let spinProps: SpinProps | undefined;
if (typeof loading === 'boolean') {
spinProps = {
spinning: loading,
};
} else if (typeof loading === 'object') {
spinProps = {
spinning: true,
...loading,
};
}
// Style
const [wrapSSR, hashId] = useStyle(prefixCls);
const wrapperClassNames = classNames(
`${prefixCls}-wrapper`,
{
[`${prefixCls}-wrapper-rtl`]: direction === 'rtl',
},
className,
hashId,
);
return wrapSSR(
<div ref={ref} className={wrapperClassNames} style={style}>
<Spin spinning={false} {...spinProps}>
{topPaginationNode}
<RcTable<RecordType>
{...tableProps}
columns={mergedColumns as RcTableProps<RecordType>['columns']}
direction={direction}
expandable={mergedExpandable}
prefixCls={prefixCls}
className={classNames({
[`${prefixCls}-middle`]: mergedSize === 'middle',
[`${prefixCls}-small`]: mergedSize === 'small',
[`${prefixCls}-bordered`]: bordered,
[`${prefixCls}-empty`]: rawData.length === 0,
})}
data={pageData}
rowKey={getRowKey}
rowClassName={internalRowClassName}
emptyText={(locale && locale.emptyText) || (renderEmpty || defaultRenderEmpty)('Table')}
// Internal
internalHooks={INTERNAL_HOOKS}
internalRefs={internalRefs as any}
transformColumns={transformColumns as RcTableProps<RecordType>['transformColumns']}
/>
{bottomPaginationNode}
</Spin>
</div>,
);
return <InternalTable<RecordType> {...props} ref={ref} _renderTimes={renderTimesRef.current} />;
}
const ForwardTable = React.forwardRef(InternalTable) as <RecordType extends object = any>(
props: React.PropsWithChildren<TableProps<RecordType>> & { ref?: React.Ref<HTMLDivElement> },
) => React.ReactElement;
type InternalTableType = typeof ForwardTable;
type CompoundedComponent = InternalTableType & {
const ForwardTable = React.forwardRef(Table) as any as RefTable & {
SELECTION_COLUMN: typeof SELECTION_COLUMN;
EXPAND_COLUMN: typeof RcTable.EXPAND_COLUMN;
EXPAND_COLUMN: typeof EXPAND_COLUMN;
SELECTION_ALL: 'SELECT_ALL';
SELECTION_INVERT: 'SELECT_INVERT';
SELECTION_NONE: 'SELECT_NONE';
@ -568,15 +33,13 @@ type CompoundedComponent = InternalTableType & {
Summary: typeof Summary;
};
const Table = ForwardTable as CompoundedComponent;
Table.SELECTION_COLUMN = SELECTION_COLUMN;
Table.EXPAND_COLUMN = RcTable.EXPAND_COLUMN;
Table.SELECTION_ALL = SELECTION_ALL;
Table.SELECTION_INVERT = SELECTION_INVERT;
Table.SELECTION_NONE = SELECTION_NONE;
Table.Column = Column;
Table.ColumnGroup = ColumnGroup;
Table.Summary = Summary;
ForwardTable.SELECTION_COLUMN = SELECTION_COLUMN;
ForwardTable.EXPAND_COLUMN = EXPAND_COLUMN;
ForwardTable.SELECTION_ALL = SELECTION_ALL;
ForwardTable.SELECTION_INVERT = SELECTION_INVERT;
ForwardTable.SELECTION_NONE = SELECTION_NONE;
ForwardTable.Column = Column;
ForwardTable.ColumnGroup = ColumnGroup;
ForwardTable.Summary = Summary;
export default Table;
export default ForwardTable;

2265
components/table/__tests__/__snapshots__/demo-extend.test.ts.snap

File diff suppressed because it is too large

2265
components/table/__tests__/__snapshots__/demo.test.ts.snap

File diff suppressed because it is too large

2
components/table/__tests__/type.test.tsx

@ -1,7 +1,7 @@
import * as React from 'react';
import type { ColumnProps } from '..';
import type { TreeColumnFilterItem } from '../hooks/useFilter/FilterDropdown';
import Table from '../Table';
import Table from '..';
const { Column, ColumnGroup } = Table;

7
components/table/demo/row-selection-debug.md

@ -0,0 +1,7 @@
## zh-CN
高损耗 `render` 性能调试。
## en-US
High cost `render` performance debugging.

88
components/table/demo/row-selection-debug.tsx

@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { Table, InputNumber } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { TableRowSelection } from 'antd/es/table/interface';
const RenderTimes = () => {
const timesRef = React.useRef(0);
timesRef.current += 1;
return <span>{timesRef.current}</span>;
};
interface DataType {
key: React.Key;
name: string;
age: number;
address: string;
}
const shouldCellUpdate = (record: any, prevRecord: any) => record !== prevRecord;
const columns: ColumnsType<DataType> = [
{
title: 'Name',
dataIndex: 'name',
shouldCellUpdate,
},
{
title: 'Age',
dataIndex: 'age',
shouldCellUpdate,
},
{
title: 'Address',
dataIndex: 'address',
shouldCellUpdate,
render: (addr) => (
<>
{addr}
<RenderTimes />
</>
),
},
];
function genData(count: number) {
const data: DataType[] = [];
for (let i = 0; i < count; i++) {
data.push({
key: i,
name: `Edward King ${i}`,
age: 32,
address: `London, Park Lane no. ${i}`,
});
}
return data;
}
const App: React.FC = () => {
const [data, setData] = useState(genData(50));
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection: TableRowSelection<DataType> = {
selectedRowKeys,
onChange: onSelectChange,
};
return (
<>
<InputNumber
value={data.length}
onChange={(cnt) => {
setData(genData(cnt || 0));
}}
/>
<Table rowSelection={rowSelection} columns={columns} dataSource={data} pagination={false} />
</>
);
};
export default App;

1
components/table/index.en-US.md

@ -67,6 +67,7 @@ const columns = [
<code src="./demo/row-selection.tsx">selection</code>
<code src="./demo/row-selection-and-operation.tsx">Selection and operation</code>
<code src="./demo/row-selection-custom.tsx">Custom selection</code>
<code src="./demo/row-selection-debug.tsx" debug>Selection Perf</code>
<code src="./demo/head.tsx">Filter and sorter</code>
<code src="./demo/filter-in-tree.tsx">Filter in Tree</code>
<code src="./demo/filter-search.tsx">Filter search</code>

3
components/table/index.ts

@ -1,4 +1,5 @@
import Table, { type TablePaginationConfig, type TableProps } from './Table';
import { type TablePaginationConfig, type TableProps } from './InternalTable';
import Table from './Table';
export type { ColumnProps } from './Column';
export type { ColumnGroupType, ColumnsType, ColumnType } from './interface';

1
components/table/index.zh-CN.md

@ -68,6 +68,7 @@ const columns = [
<code src="./demo/row-selection.tsx">可选择</code>
<code src="./demo/row-selection-and-operation.tsx">选择和操作</code>
<code src="./demo/row-selection-custom.tsx">自定义选择项</code>
<code src="./demo/row-selection-debug.tsx" debug>选择性能</code>
<code src="./demo/head.tsx">筛选和排序</code>
<code src="./demo/filter-in-tree.tsx">树型筛选菜单</code>
<code src="./demo/filter-search.tsx">自定义筛选的搜索</code>

11
components/table/interface.ts

@ -10,6 +10,17 @@ import type { PaginationProps } from '../pagination';
import type { TooltipProps } from '../tooltip';
import type { Breakpoint } from '../_util/responsiveObserver';
import type { INTERNAL_SELECTION_ITEM } from './hooks/useSelection';
import type { InternalTableProps, TableProps } from './InternalTable';
export type RefTable = <RecordType extends object = any>(
props: React.PropsWithChildren<TableProps<RecordType>> & { ref?: React.Ref<HTMLDivElement> },
) => React.ReactElement;
export type RefInternalTable = <RecordType extends object = any>(
props: React.PropsWithChildren<InternalTableProps<RecordType>> & {
ref?: React.Ref<HTMLDivElement>;
},
) => React.ReactElement;
export { GetRowKey, ExpandableConfig };

2
package.json

@ -142,7 +142,7 @@
"rc-slider": "~10.0.0",
"rc-steps": "~6.0.0",
"rc-switch": "~4.0.0",
"rc-table": "~7.29.1",
"rc-table": "~7.30.2",
"rc-tabs": "~12.5.1",
"rc-textarea": "~0.4.5",
"rc-tooltip": "~5.2.0",

Loading…
Cancel
Save