From 0a19aab1107e09a2602f022a64a9a1fadd6b11d4 Mon Sep 17 00:00:00 2001 From: tewarig Date: Fri, 10 Jan 2025 02:35:02 +0530 Subject: [PATCH] chore: blade table memo poc --- .../blade/src/components/Table/Table.web.tsx | 682 +++++++++--------- .../src/components/Table/TableBody.web.tsx | 226 +++--- .../src/components/Table/TableHeader.web.tsx | 5 + .../components/Table/TablePagination.web.tsx | 8 +- .../Table/docs/BasicTable.stories.tsx | 306 ++++---- packages/blade/src/components/Table/index.ts | 2 +- packages/blade/src/components/Table/types.ts | 2 +- .../src/utils/useWhyDidYouRender/index.ts | 29 + 8 files changed, 708 insertions(+), 552 deletions(-) create mode 100644 packages/blade/src/utils/useWhyDidYouRender/index.ts diff --git a/packages/blade/src/components/Table/Table.web.tsx b/packages/blade/src/components/Table/Table.web.tsx index 10a3cf1da78..4b8b3740cdd 100644 --- a/packages/blade/src/components/Table/Table.web.tsx +++ b/packages/blade/src/components/Table/Table.web.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, memo } from 'react'; import { Table as ReactTable } from '@table-library/react-table-library/table'; import { useTheme as useTableTheme } from '@table-library/react-table-library/theme'; import type { MiddlewareFunction } from '@table-library/react-table-library/types/common'; @@ -11,6 +11,7 @@ import { } from '@table-library/react-table-library/select'; import styled from 'styled-components'; import usePresence from 'use-presence'; +import isEqual from 'lodash/isEqual'; import type { TableContextType } from './TableContext'; import { TableContext } from './TableContext'; import { ComponentIds } from './componentIds'; @@ -43,6 +44,7 @@ import getIn from '~utils/lodashButBetter/get'; import { makeAccessible } from '~utils/makeAccessible'; import { useIsMobile } from '~utils/useIsMobile'; import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; +import useWhyDidYouRender from '~utils/useWhyDidYouRender'; const rowSelectType: Record< NonNullable['selectionType']>, @@ -120,62 +122,86 @@ const RefreshWrapper = styled(BaseBox)<{ }; }); -const _Table = ({ - children, - data, - multiSelectTrigger = 'row', - selectionType = 'none', - onSelectionChange, - isHeaderSticky, - isFooterSticky, - isFirstColumnSticky, - rowDensity = 'normal', - onSortChange, - sortFunctions, - toolbar, - pagination, - height, - showStripedRows, - gridTemplateColumns, - isLoading = false, - isRefreshing = false, - showBorderedCells = false, - defaultSelectedIds = [], - ...rest -}: TableProps): React.ReactElement => { - const { theme } = useTheme(); - const [selectedRows, setSelectedRows] = React.useState['id'][]>( - selectionType !== 'none' ? defaultSelectedIds : [], - ); - const [disabledRows, setDisabledRows] = React.useState['id'][]>([]); - const [totalItems, setTotalItems] = React.useState(data.nodes.length || 0); - const [paginationType, setPaginationType] = React.useState>( - 'client', - ); - const [headerRowDensity, setHeaderRowDensity] = React.useState( - undefined, - ); - const [hasHoverActions, setHasHoverActions] = React.useState(false); - // Need to make header is sticky if first column is sticky otherwise the first header cell will not be sticky - const shouldHeaderBeSticky = isHeaderSticky ?? isFirstColumnSticky; - const backgroundColor = tableBackgroundColor; - - const isMobile = useIsMobile(); - const lastHoverActionsColWidth = isMobile ? '1fr' : '0px'; - - const { - isEntering: isRefreshSpinnerEntering, - isMounted: isRefreshSpinnerMounted, - isExiting: isRefreshSpinnerExiting, - isVisible: isRefreshSpinnerVisible, - } = usePresence(isRefreshing, { - transitionDuration: theme.motion.duration.quick, - }); - - // Table Theme - const columnCount = getTableHeaderCellCount(children); - const firstColumnStickyHeaderCellCSS = isFirstColumnSticky - ? ` +const _Table = memo( + ({ + children, + data, + multiSelectTrigger = 'row', + selectionType = 'none', + onSelectionChange, + isHeaderSticky, + isFooterSticky, + isFirstColumnSticky, + rowDensity = 'normal', + onSortChange, + sortFunctions, + toolbar, + pagination, + height, + showStripedRows, + gridTemplateColumns, + isLoading = false, + isRefreshing = false, + showBorderedCells = false, + defaultSelectedIds = [], + ...rest + }: TableProps): React.ReactElement => { + useWhyDidYouRender('table', { + children, + data, + multiSelectTrigger, + selectionType, + onSelectionChange, + isHeaderSticky, + isFooterSticky, + isFirstColumnSticky, + rowDensity, + onSortChange, + sortFunctions, + toolbar, + pagination, + height, + showStripedRows, + gridTemplateColumns, + isLoading, + isRefreshing, + showBorderedCells, + defaultSelectedIds, + ...rest, + }); + const { theme } = useTheme(); + const [selectedRows, setSelectedRows] = React.useState['id'][]>( + selectionType !== 'none' ? defaultSelectedIds : [], + ); + const [disabledRows, setDisabledRows] = React.useState['id'][]>([]); + const [totalItems, setTotalItems] = React.useState(data.nodes.length || 0); + const [paginationType, setPaginationType] = React.useState>( + 'client', + ); + const [headerRowDensity, setHeaderRowDensity] = React.useState< + TableHeaderRowProps['rowDensity'] + >(undefined); + const [hasHoverActions, setHasHoverActions] = React.useState(false); + // Need to make header is sticky if first column is sticky otherwise the first header cell will not be sticky + const shouldHeaderBeSticky = isHeaderSticky ?? isFirstColumnSticky; + const backgroundColor = tableBackgroundColor; + + const isMobile = useIsMobile(); + const lastHoverActionsColWidth = isMobile ? '1fr' : '0px'; + + const { + isEntering: isRefreshSpinnerEntering, + isMounted: isRefreshSpinnerMounted, + isExiting: isRefreshSpinnerExiting, + isVisible: isRefreshSpinnerVisible, + } = usePresence(isRefreshing, { + transitionDuration: theme.motion.duration.quick, + }); + + // Table Theme + const columnCount = getTableHeaderCellCount(children); + const firstColumnStickyHeaderCellCSS = isFirstColumnSticky + ? ` &:nth-of-type(1) { left: 0 !important; position: sticky !important; @@ -190,9 +216,9 @@ const _Table = ({ } ` }` - : ''; - const firstColumnStickyFooterCellCSS = isFirstColumnSticky - ? ` + : ''; + const firstColumnStickyFooterCellCSS = isFirstColumnSticky + ? ` &:nth-of-type(1) { left: 0 !important; position: sticky !important; @@ -207,9 +233,9 @@ const _Table = ({ } ` }` - : ''; - const firstColumnStickyBodyCellCSS = isFirstColumnSticky - ? ` + : ''; + const firstColumnStickyBodyCellCSS = isFirstColumnSticky + ? ` &:nth-of-type(1) { left: 0 !important; position: sticky !important; @@ -224,14 +250,14 @@ const _Table = ({ } ` }` - : ''; + : ''; - const tableTheme = useTableTheme({ - Table: ` + const tableTheme = useTableTheme({ + Table: ` height:${isFooterSticky ? `100%` : undefined}; border: ${makeBorderSize(theme.border.width.thin)} solid ${ - theme.colors.surface.border.gray.muted - }; + theme.colors.surface.border.gray.muted + }; --data-table-library_grid-template-columns: ${ gridTemplateColumns ? `${gridTemplateColumns} ${hasHoverActions ? lastHoverActionsColWidth : ''}` @@ -243,290 +269,296 @@ const _Table = ({ } !important; background-color: ${getIn(theme.colors, backgroundColor)}; `, - HeaderCell: ` + HeaderCell: ` position: ${shouldHeaderBeSticky ? 'sticky' : 'relative'}; top: ${shouldHeaderBeSticky ? '0' : undefined}; ${firstColumnStickyHeaderCellCSS} `, - Cell: ` + Cell: ` ${firstColumnStickyBodyCellCSS} `, - FooterCell: ` + FooterCell: ` position: ${isFooterSticky ? 'sticky' : 'relative'}; bottom: ${isFooterSticky ? '0' : undefined}; ${firstColumnStickyFooterCellCSS} `, - }); - - useEffect(() => { - // Get the total number of items - setTotalItems(data.nodes.length); - }, [data.nodes]); - - // Selection Logic - const onSelectChange: MiddlewareFunction = (action, state): void => { - const selectedIds: Identifier[] = state.id ? [state.id] : state.ids ?? []; - setSelectedRows(selectedIds); - onSelectionChange?.({ - selectedIds, - values: data.nodes.filter((node) => selectedIds.includes(node.id)), }); - }; - const rowSelectConfig = useRowSelect( - data, - { - onChange: onSelectChange, - state: { - ...(selectionType === 'multiple' - ? { ids: selectedRows } - : selectionType === 'single' - ? { id: selectedRows[0] } - : {}), + useEffect(() => { + // Get the total number of items + setTotalItems(data.nodes.length); + }, [data.nodes]); + + // Selection Logic + const onSelectChange: MiddlewareFunction = (action, state): void => { + const selectedIds: Identifier[] = state.id ? [state.id] : state.ids ?? []; + setSelectedRows(selectedIds); + onSelectionChange?.({ + selectedIds, + values: data.nodes.filter((node) => selectedIds.includes(node.id)), + }); + }; + + const rowSelectConfig = useRowSelect( + data, + { + onChange: onSelectChange, + state: { + ...(selectionType === 'multiple' + ? { ids: selectedRows } + : selectionType === 'single' + ? { id: selectedRows[0] } + : {}), + }, }, - }, - { - clickType: - multiSelectTrigger === 'row' ? SelectClickTypes.RowClick : SelectClickTypes.ButtonClick, - rowSelect: selectionType !== 'none' ? rowSelectType[selectionType] : undefined, - }, - ); - - const toggleRowSelectionById = useMemo( - () => (id: Identifier): void => { - rowSelectConfig.fns.onToggleById(id); - }, - [rowSelectConfig.fns], - ); - - const deselectAllRows = useMemo( - () => (): void => { - rowSelectConfig.fns.onRemoveAll(); - }, - [rowSelectConfig.fns], - ); - - const toggleAllRowsSelection = useMemo( - () => (): void => { - if (selectedRows.length > 0) { - rowSelectConfig.fns.onRemoveAll(); - } else { - const ids = data.nodes - .map((item: TableNode) => (disabledRows.includes(item.id) ? null : item.id)) - .filter(Boolean) as Identifier[]; + { + clickType: + multiSelectTrigger === 'row' ? SelectClickTypes.RowClick : SelectClickTypes.ButtonClick, + rowSelect: selectionType !== 'none' ? rowSelectType[selectionType] : undefined, + }, + ); - rowSelectConfig.fns.onAddAll(ids); - } - }, - [rowSelectConfig.fns, data.nodes, selectedRows, disabledRows], - ); - - // Sort Logic - const handleSortChange: MiddlewareFunction = (action, state) => { - onSortChange?.({ - sortKey: state.sortKey, - isSortReversed: state.reverse, - }); - }; + const toggleRowSelectionById = useMemo( + () => (id: Identifier): void => { + rowSelectConfig.fns.onToggleById(id); + }, + [rowSelectConfig.fns], + ); - const sort = useSort( - data, - { - onChange: handleSortChange, - }, - { - // @ts-expect-error ignore this, if sortFunctions is undefined, it will be ignored - sortFns: sortFunctions, - }, - ); - - const currentSortedState: TableContextType['currentSortedState'] = useMemo(() => { - return { - sortKey: sort.state.sortKey, - isSortReversed: sort.state.reverse, - sortableColumns: Object.keys(sortFunctions ?? {}), + const deselectAllRows = useMemo( + () => (): void => { + rowSelectConfig.fns.onRemoveAll(); + }, + [rowSelectConfig.fns], + ); + + const toggleAllRowsSelection = useMemo( + () => (): void => { + if (selectedRows.length > 0) { + rowSelectConfig.fns.onRemoveAll(); + } else { + const ids = data.nodes + .map((item: TableNode) => (disabledRows.includes(item.id) ? null : item.id)) + .filter(Boolean) as Identifier[]; + + rowSelectConfig.fns.onAddAll(ids); + } + }, + [rowSelectConfig.fns, data.nodes, selectedRows, disabledRows], + ); + + // Sort Logic + const handleSortChange: MiddlewareFunction = (action, state) => { + onSortChange?.({ + sortKey: state.sortKey, + isSortReversed: state.reverse, + }); }; - }, [sort.state, sortFunctions]); - const toggleSort = useCallback( - (sortKey: string): void => { - sort.fns.onToggleSort({ - sortKey, - }); - }, - [sort.fns], - ); + const sort = useSort( + data, + { + onChange: handleSortChange, + }, + { + // @ts-expect-error ignore this, if sortFunctions is undefined, it will be ignored + sortFns: sortFunctions, + }, + ); + + const currentSortedState: TableContextType['currentSortedState'] = useMemo(() => { + return { + sortKey: sort.state.sortKey, + isSortReversed: sort.state.reverse, + sortableColumns: Object.keys(sortFunctions ?? {}), + }; + }, [sort.state, sortFunctions]); + + const toggleSort = useCallback( + (sortKey: string): void => { + sort.fns.onToggleSort({ + sortKey, + }); + }, + [sort.fns], + ); - // Pagination + // Pagination - const hasPagination = Boolean(pagination); + const hasPagination = Boolean(pagination); - const paginationConfig = usePagination( - data, - { - state: { - page: 0, - size: tablePagination.defaultPageSize, + const paginationConfig = usePagination( + data, + { + state: { + page: 0, + size: tablePagination.defaultPageSize, + }, }, - }, - { - isServer: paginationType === 'server', - }, - ); - - const currentPaginationState = useMemo(() => { - return hasPagination - ? { - page: paginationConfig.state.page, - size: paginationConfig.state.size, - } - : undefined; - }, [paginationConfig.state, hasPagination]); - - const setPaginationPage = useCallback( - (page: number): void => { - paginationConfig.fns.onSetPage(page); - }, - [paginationConfig.fns], - ); - - const setPaginationRowSize = useCallback( - (size: number): void => { - paginationConfig.fns.onSetSize(size); - }, - [paginationConfig.fns], - ); - - // Toolbar Component - if (__DEV__) { - if (toolbar && !isValidAllowedChildren(toolbar, ComponentIds.TableToolbar)) { - throwBladeError({ - message: 'Only TableToolbar component is allowed in the `toolbar` prop', - moduleName: 'Table', - }); + { + isServer: paginationType === 'server', + }, + ); + + const currentPaginationState = useMemo(() => { + return hasPagination + ? { + page: paginationConfig.state.page, + size: paginationConfig.state.size, + } + : undefined; + }, [paginationConfig.state, hasPagination]); + + const setPaginationPage = useCallback( + (page: number): void => { + paginationConfig.fns.onSetPage(page); + }, + [paginationConfig.fns], + ); + + const setPaginationRowSize = useCallback( + (size: number): void => { + paginationConfig.fns.onSetSize(size); + }, + [paginationConfig.fns], + ); + + // Toolbar Component + if (__DEV__) { + if (toolbar && !isValidAllowedChildren(toolbar, ComponentIds.TableToolbar)) { + throwBladeError({ + message: 'Only TableToolbar component is allowed in the `toolbar` prop', + moduleName: 'Table', + }); + } } - } - // Table Context - const tableContext: TableContextType = useMemo( - () => ({ - selectionType, - selectedRows, - totalItems, - toggleRowSelectionById, - toggleAllRowsSelection, - deselectAllRows, - rowDensity, - toggleSort, - currentSortedState, - setPaginationPage, - setPaginationRowSize, - currentPaginationState, - showStripedRows, - disabledRows, - setDisabledRows, - paginationType, - setPaginationType, - backgroundColor, - headerRowDensity, - setHeaderRowDensity, - showBorderedCells, - hasHoverActions, - setHasHoverActions, - }), - [ - selectionType, - selectedRows, - totalItems, - toggleRowSelectionById, - toggleAllRowsSelection, - deselectAllRows, - rowDensity, - toggleSort, - currentSortedState, - setPaginationPage, - setPaginationRowSize, - currentPaginationState, - showStripedRows, - disabledRows, - setDisabledRows, - paginationType, - setPaginationType, - backgroundColor, - headerRowDensity, - setHeaderRowDensity, - showBorderedCells, - hasHoverActions, - setHasHoverActions, - ], - ); - - return ( - - {isLoading ? ( - - - - ) : ( - - {isRefreshSpinnerMounted && ( - - - - )} - {toolbar} - ({ + selectionType, + selectedRows, + totalItems, + toggleRowSelectionById, + toggleAllRowsSelection, + deselectAllRows, + rowDensity, + toggleSort, + currentSortedState, + setPaginationPage, + setPaginationRowSize, + currentPaginationState, + showStripedRows, + disabledRows, + setDisabledRows, + paginationType, + setPaginationType, + backgroundColor, + headerRowDensity, + setHeaderRowDensity, + showBorderedCells, + hasHoverActions, + setHasHoverActions, + }), + [ + selectionType, + selectedRows, + totalItems, + toggleRowSelectionById, + toggleAllRowsSelection, + deselectAllRows, + rowDensity, + toggleSort, + currentSortedState, + setPaginationPage, + setPaginationRowSize, + currentPaginationState, + showStripedRows, + disabledRows, + setDisabledRows, + paginationType, + setPaginationType, + backgroundColor, + headerRowDensity, + setHeaderRowDensity, + showBorderedCells, + hasHoverActions, + setHasHoverActions, + ], + ); + + return ( + + {isLoading ? ( + - {children} - - {pagination} - - )} - - ); -}; + + + ) : ( + + {isRefreshSpinnerMounted && ( + + + + )} + {toolbar} + + + {children} + + + {pagination} + + )} + + ); + }, + (prevProps, nextProps) => { + return isEqual(prevProps, nextProps); + }, +); const Table = assignWithoutSideEffects(_Table, { componentId: ComponentIds.Table, diff --git a/packages/blade/src/components/Table/TableBody.web.tsx b/packages/blade/src/components/Table/TableBody.web.tsx index 717924ebbdb..69ca01505d1 100644 --- a/packages/blade/src/components/Table/TableBody.web.tsx +++ b/packages/blade/src/components/Table/TableBody.web.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, memo } from 'react'; import { Body, Row, Cell } from '@table-library/react-table-library/table'; import styled from 'styled-components'; import { useTableContext } from './TableContext'; @@ -12,6 +12,7 @@ import type { TableBackgroundColors, } from './types'; import getIn from '~utils/lodashButBetter/get'; +import isEqual from 'lodash/isEqual'; import type { DotNotationToken } from '~utils/lodashButBetter/get'; import { Text } from '~components/Typography'; import type { CheckboxProps } from '~components/Checkbox'; @@ -26,6 +27,7 @@ import { makeAccessible } from '~utils/makeAccessible'; import { useIsomorphicLayoutEffect } from '~utils/useIsomorphicLayoutEffect'; import type { Theme } from '~components/BladeProvider'; import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; +import useWhyDidYouRender from '~utils/useWhyDidYouRender'; const getTableRowBackgroundTransition = (theme: Theme): string => { const rowBackgroundTransition = `background-color ${makeMotionTime( @@ -218,22 +220,31 @@ const StyledBody = styled(Body)<{ }; }); -const _TableBody = ({ children, ...rest }: TableBodyProps): React.ReactElement => { - const { showStripedRows, selectionType } = useTableContext(); - const isSelectable = selectionType !== 'none'; - - return ( - - {children} - - ); -}; +const _TableBody = memo( + ({ children, ...rest }: TableBodyProps): React.ReactElement => { + const { showStripedRows, selectionType } = useTableContext(); + const isSelectable = selectionType !== 'none'; + useWhyDidYouRender('TableBody', { + children, + ...rest, + }); + + return ( + + {children} + + ); + }, + (prevProps, nextProps) => { + return isEqual(prevProps.children, nextProps.children); + }, +); const TableBody = assignWithoutSideEffects(_TableBody, { componentId: ComponentIds.TableBody, @@ -440,101 +451,116 @@ const StyledRow = styled(Row)<{ }; }); -const _TableRow = ({ - children, - item, - isDisabled, - onHover, - onClick, - hoverActions, - testID, - ...rest -}: TableRowProps): React.ReactElement => { - const { - selectionType, - selectedRows, - toggleRowSelectionById, - setDisabledRows, - showBorderedCells, - setHasHoverActions, - } = useTableContext(); - const isSelectable = selectionType !== 'none'; - const isMultiSelect = selectionType === 'multiple'; - const isSelected = selectedRows?.includes(item.id); - const hasHoverActions = Boolean(hoverActions); - - useEffect(() => { - if (isDisabled) { - setDisabledRows((prev) => [...prev, item.id]); - } - }, [isDisabled, item.id, setDisabledRows]); - - useIsomorphicLayoutEffect(() => { - if (hasHoverActions) { - setHasHoverActions(true); - } - }, [hasHoverActions]); - - return ( - onHover?.({ item })} - onClick={() => onClick?.({ item })} - {...makeAccessible({ selected: isSelected })} - {...metaAttribute({ name: MetaConstants.TableRow, testID })} - {...makeAnalyticsAttribute(rest)} - > - {isMultiSelect && ( - !isDisabled && toggleRowSelectionById(item.id)} - isDisabled={isDisabled} - /> - )} - {children} - {hoverActions ? ( - - +const _TableRow = memo( + ({ + children, + item, + isDisabled, + onHover, + onClick, + hoverActions, + testID, + ...rest + }: TableRowProps): React.ReactElement => { + useWhyDidYouRender('TableRow', { + children, + item, + isDisabled, + onHover, + onClick, + hoverActions, + testID, + ...rest, + }); + const { + selectionType, + selectedRows, + toggleRowSelectionById, + setDisabledRows, + showBorderedCells, + setHasHoverActions, + } = useTableContext(); + const isSelectable = selectionType !== 'none'; + const isMultiSelect = selectionType === 'multiple'; + const isSelected = selectedRows?.includes(item.id); + const hasHoverActions = Boolean(hoverActions); + + useEffect(() => { + if (isDisabled) { + setDisabledRows((prev) => [...prev, item.id]); + } + }, [isDisabled, item.id, setDisabledRows]); + + useIsomorphicLayoutEffect(() => { + if (hasHoverActions) { + setHasHoverActions(true); + } + }, [hasHoverActions]); + + return ( + onHover?.({ item })} + onClick={() => onClick?.({ item })} + {...makeAccessible({ selected: isSelected })} + {...metaAttribute({ name: MetaConstants.TableRow, testID })} + {...makeAnalyticsAttribute(rest)} + > + {isMultiSelect && ( + !isDisabled && toggleRowSelectionById(item.id)} + isDisabled={isDisabled} + /> + )} + {children} + {hoverActions ? ( + - {hoverActions} + + {hoverActions} + - - - ) : null} - - ); -}; + + ) : null} + + ); + }, + (prevProps, nextProps) => { + return isEqual(prevProps, nextProps); + }, +); const TableRow = assignWithoutSideEffects(_TableRow, { componentId: ComponentIds.TableRow, diff --git a/packages/blade/src/components/Table/TableHeader.web.tsx b/packages/blade/src/components/Table/TableHeader.web.tsx index c7e7611e148..11febdaf0a2 100644 --- a/packages/blade/src/components/Table/TableHeader.web.tsx +++ b/packages/blade/src/components/Table/TableHeader.web.tsx @@ -23,6 +23,7 @@ import getIn from '~utils/lodashButBetter/get'; import { getFocusRingStyles } from '~utils/getFocusRingStyles'; import { size } from '~tokens/global'; import { makeAnalyticsAttribute } from '~utils/makeAnalyticsAttribute'; +import useWhyDidYouRender from '~utils/useWhyDidYouRender'; const SortButton = styled.button(({ theme }) => ({ cursor: 'pointer', @@ -77,6 +78,10 @@ const StyledHeader = styled(Header)({ }); const _TableHeader = ({ children, ...rest }: TableHeaderRowProps): React.ReactElement => { + useWhyDidYouRender('TableHeader', { + children, + ...rest, + }); return ( [] = [10, 25, 50]; +const pageSizeOptions: NonNullable[] = [ + 10, + 25, + 50, + 100, + 200, +]; const PageSelectionButton = styled.button<{ isSelected?: boolean }>(({ theme, isSelected }) => ({ backgroundColor: isSelected diff --git a/packages/blade/src/components/Table/docs/BasicTable.stories.tsx b/packages/blade/src/components/Table/docs/BasicTable.stories.tsx index d9f3c6831d9..10d7d582c67 100644 --- a/packages/blade/src/components/Table/docs/BasicTable.stories.tsx +++ b/packages/blade/src/components/Table/docs/BasicTable.stories.tsx @@ -1,4 +1,5 @@ import type { StoryFn, Meta } from '@storybook/react'; +import React, { useState, useCallback, useMemo, memo } from 'react'; import type { TableData, TableProps } from '../types'; import { Table as TableComponent, @@ -25,6 +26,7 @@ import { getStyledPropsArgTypes } from '~components/Box/BaseBox/storybookArgType import { Button } from '~components/Button'; import { IconButton } from '~components/Button/IconButton'; import { CheckIcon, CloseIcon } from '~components/Icons'; +import useWhyDidYouRender from '~utils/useWhyDidYouRender'; export default { title: 'Components/Table', @@ -71,7 +73,7 @@ export default { } as Meta>; const nodes: Item[] = [ - ...Array.from({ length: 100 }, (_, i) => ({ + ...Array.from({ length: 160 }, (_, i) => ({ id: (i + 1).toString(), paymentId: `rzp${Math.floor(Math.random() * 1000000)}`, amount: Number((Math.random() * 10000).toFixed(2)), @@ -110,138 +112,194 @@ const data: TableData = { nodes, }; +const TableRowContent = memo(({ tableItem }) => { + return ( + <> + + {tableItem.paymentId} + + + {tableItem.account} + + {tableItem.date?.toLocaleDateString('en-IN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + })} + + {tableItem.method} + + + {tableItem.status} + + + + ); +}); +const MemoizedTableRow = memo(({ tableItem, index, onClick, hoverActions }) => ( + + + +)); + +const MemoizedTableBody = memo(({ tableData, callbackedOnClick, memomizedHoverActions }) => { + useWhyDidYouRender('TableBody', { tableData, callbackedOnClick, memomizedHoverActions }); + const memoizedRows = useMemo(() => { + return tableData.map((tableItem, index) => ( + + )); + }, [tableData, callbackedOnClick, memomizedHoverActions]); + return {memoizedRows}; +}); + +const MemoziedToolbar = memo(() => { + return ( + + + + + + + ); +}); const TableTemplate: StoryFn = ({ ...args }) => { + const [selectedValues, setSelected] = React.useState([]); + const callbackedOnClick = useCallback(() => { + console.log('clicked'); + }, []); + const memomizedHoverActions = useMemo( + () => ( + <> + + { + console.log('Approved'); + }} + /> + { + console.log('Rejected'); + }} + /> + + ), + [], + ); + const memorizedTableData = useMemo(() => [...data.nodes], []); + + const memorizedTableHeaderRows = useMemo( + () => ( + + ID + Amount + Account + Date + Method + Status + + ), + [], + ); + const memoizedRenderFunction = useMemo(() => { + return ( + <> + {memorizedTableHeaderRows} + + + ); + }, [memorizedTableHeaderRows, memorizedTableData, callbackedOnClick, memomizedHoverActions]); + + const memorziedSortFunctions = useMemo( + () => ({ + ID: (array: Item[]) => array.sort((a, b) => Number(a.id) - Number(b.id)), + AMOUNT: (array: Item[]) => array.sort((a, b) => a.amount - b.amount), + PAYMENT_ID: (array: Item[]) => array.sort((a, b) => a.paymentId.localeCompare(b.paymentId)), + DATE: (array: Item[]) => array.sort((a, b) => a.date.getTime() - b.date.getTime()), + STATUS: (array: Item[]) => array.sort((a, b) => a.status.localeCompare(b.status)), + }), + [], + ); + + const memorizedPagination = useMemo(() => { + return ( + + ); + }, []); + + const memoziedDefaultSelectedIds = useMemo(() => ['1', '3'], []); + const onSelectionChangeCallback = useCallback(({ values }) => { + setSelected(values); + }, []); + const tableComponentCallback = useCallback(() => { + return ( + <> + {memorizedTableHeaderRows} + + + ); + }, [callbackedOnClick, memomizedHoverActions, memorizedTableData, memorizedTableHeaderRows]); + return ( - - - - - - } - sortFunctions={{ - ID: (array) => array.sort((a, b) => Number(a.id) - Number(b.id)), - AMOUNT: (array) => array.sort((a, b) => a.amount - b.amount), - PAYMENT_ID: (array) => array.sort((a, b) => a.paymentId.localeCompare(b.paymentId)), - DATE: (array) => array.sort((a, b) => a.date.getTime() - b.date.getTime()), - STATUS: (array) => array.sort((a, b) => a.status.localeCompare(b.status)), - }} - pagination={ - - } + // onSelectionChange={({ values }) => setSelected(values)} + selectionType="multiple" + // toolbar={} + sortFunctions={memorziedSortFunctions} + pagination={memorizedPagination} > - {(tableData) => ( - <> - - - ID - Amount - Account - Date - Method - Status - - - - {tableData.map((tableItem, index) => ( - - - { - console.log('Approved', tableItem.id); - }} - /> - { - console.log('Rejected', tableItem.id); - }} - /> - - } - onClick={() => { - console.log('where'); - }} - > - - {tableItem.paymentId} - - - {tableItem.account} - - {tableItem.date?.toLocaleDateString('en-IN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - })} - - {tableItem.method} - - - {tableItem.status} - - - - ))} - - - - Total - - - - - - - - - {args.selectionType === 'multiple' ? - : null} - - - - - - - )} + {tableComponentCallback} ); diff --git a/packages/blade/src/components/Table/index.ts b/packages/blade/src/components/Table/index.ts index 5485e99cc48..7d834c2882d 100644 --- a/packages/blade/src/components/Table/index.ts +++ b/packages/blade/src/components/Table/index.ts @@ -1,4 +1,4 @@ -export { Table } from './Table'; +export { Table, TableVirtulized } from './Table'; export { TableHeader, TableHeaderCell, TableHeaderRow } from './TableHeader'; export { TableBody, TableCell, TableRow } from './TableBody'; export { TableFooter, TableFooterCell, TableFooterRow } from './TableFooter'; diff --git a/packages/blade/src/components/Table/types.ts b/packages/blade/src/components/Table/types.ts index 771ee7efd17..c6a3e39c5a7 100644 --- a/packages/blade/src/components/Table/types.ts +++ b/packages/blade/src/components/Table/types.ts @@ -346,7 +346,7 @@ type TablePaginationCommonProps = { * Page size controls how rows are shown per page. * @default 10 **/ - defaultPageSize?: 10 | 25 | 50; + defaultPageSize?: 10 | 25 | 50 | 100 | 200; /** * The current page. Passing this prop will make the component controlled and will not update the page on its own. **/ diff --git a/packages/blade/src/utils/useWhyDidYouRender/index.ts b/packages/blade/src/utils/useWhyDidYouRender/index.ts new file mode 100644 index 00000000000..42fb80dd3d4 --- /dev/null +++ b/packages/blade/src/utils/useWhyDidYouRender/index.ts @@ -0,0 +1,29 @@ +// use why did you re render tells why a component re-rendered +import { useEffect, useRef } from 'react'; + +function useWhyDidYouRender(componentName: string, props: Record): void { + const previousProps = useRef(props); + + useEffect(() => { + if (previousProps.current) { + const changes: Record = {}; + + Object.keys(props).forEach((key) => { + if (props[key] !== previousProps.current[key]) { + changes[key] = { + from: previousProps.current[key], + to: props[key], + }; + } + }); + + if (Object.keys(changes).length > 0) { + console.log(`[${componentName}] component re-rendered due to:`, changes); + } + } + + previousProps.current = props; + }, [props, componentName]); +} + +export default useWhyDidYouRender;