From 3a0f02f2f22c68007fd6f2ea93a6cc32161570ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Mon, 4 Sep 2023 13:33:02 +0200 Subject: [PATCH] feat: table virtualization (#1408) * feat: poc table virtualization * feat: table virtualization * feat: add overscan of 15 * fix: increase overscan to 50 * fix: dead code * fix: debug mode * feat: styled space --- front/package.json | 1 + .../modules/apollo/hooks/useApolloFactory.ts | 1 + .../ui/table/components/EntityTableBody.tsx | 65 ++++++++++++++++--- .../ui/table/components/EntityTableRow.tsx | 13 +++- .../scroll/components/ScrollWrapper.tsx | 14 ++-- .../scroll/hooks/useScrollWrapperScopedRef.ts | 14 ++++ front/yarn.lock | 20 ++++++ 7 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts diff --git a/front/package.json b/front/package.json index 0c1663c75..d1372bacb 100644 --- a/front/package.json +++ b/front/package.json @@ -16,6 +16,7 @@ "@hello-pangea/dnd": "^16.2.0", "@hookform/resolvers": "^3.1.1", "@tabler/icons-react": "^2.30.0", + "@tanstack/react-virtual": "^3.0.0-alpha.0", "@types/node": "^16.18.4", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", diff --git a/front/src/modules/apollo/hooks/useApolloFactory.ts b/front/src/modules/apollo/hooks/useApolloFactory.ts index 054bff601..7e996b531 100644 --- a/front/src/modules/apollo/hooks/useApolloFactory.ts +++ b/front/src/modules/apollo/hooks/useApolloFactory.ts @@ -44,6 +44,7 @@ export function useApolloFactory() { fetchPolicy: 'cache-first', }, }, + connectToDevTools: isDebugMode, // We don't want to re-create the client on token change or it will cause infinite loop initialTokenPair: tokenPair, onTokenPairChange(tokenPair) { diff --git a/front/src/modules/ui/table/components/EntityTableBody.tsx b/front/src/modules/ui/table/components/EntityTableBody.tsx index ce6ec88b5..ecf54bdea 100644 --- a/front/src/modules/ui/table/components/EntityTableBody.tsx +++ b/front/src/modules/ui/table/components/EntityTableBody.tsx @@ -1,6 +1,9 @@ +import styled from '@emotion/styled'; +import { useVirtual } from '@tanstack/react-virtual'; import { useRecoilValue } from 'recoil'; import { isNavbarSwitchingSizeState } from '@/ui/layout/states/isNavbarSwitchingSizeState'; +import { useScrollWrapperScopedRef } from '@/ui/utilities/scroll/hooks/useScrollWrapperScopedRef'; import { RowIdContext } from '../contexts/RowIdContext'; import { RowIndexContext } from '../contexts/RowIndexContext'; @@ -9,28 +12,70 @@ import { tableRowIdsState } from '../states/tableRowIdsState'; import { EntityTableRow } from './EntityTableRow'; +type SpaceProps = { + top?: number; + bottom?: number; +}; + +const StyledSpace = styled.td` + ${({ top }) => top && `padding-top: ${top}px;`} + ${({ bottom }) => bottom && `padding-bottom: ${bottom}px;`} +`; + export function EntityTableBody() { + const scrollWrapperRef = useScrollWrapperScopedRef(); + const rowIds = useRecoilValue(tableRowIdsState); const isNavbarSwitchingSize = useRecoilValue(isNavbarSwitchingSizeState); - const isFetchingEntityTableData = useRecoilValue( isFetchingEntityTableDataState, ); + const rowVirtualizer = useVirtual({ + size: rowIds.length, + parentRef: scrollWrapperRef, + overscan: 50, + }); + + const items = rowVirtualizer.virtualItems; + const paddingTop = items.length > 0 ? items[0].start : 0; + const paddingBottom = + items.length > 0 + ? rowVirtualizer.totalSize - items[items.length - 1].end + : 0; + if (isFetchingEntityTableData || isNavbarSwitchingSize) { return null; } return ( - - {rowIds.map((rowId, index) => ( - - - - - - ))} - + <> + {paddingTop > 0 && ( + + + + )} + {items.map((virtualItem) => { + const rowId = rowIds[virtualItem.index]; + + return ( + + + + + + ); + })} + {paddingBottom > 0 && ( + + + + )} + ); } diff --git a/front/src/modules/ui/table/components/EntityTableRow.tsx b/front/src/modules/ui/table/components/EntityTableRow.tsx index 111bfd4fc..ebf1c22bc 100644 --- a/front/src/modules/ui/table/components/EntityTableRow.tsx +++ b/front/src/modules/ui/table/components/EntityTableRow.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import styled from '@emotion/styled'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; @@ -15,7 +16,14 @@ const StyledRow = styled.tr<{ selected: boolean }>` props.selected ? props.theme.accent.quaternary : 'none'}; `; -export function EntityTableRow({ rowId }: { rowId: string }) { +type EntityTableRowProps = { + rowId: string; +}; + +export const EntityTableRow = forwardRef< + HTMLTableRowElement, + EntityTableRowProps +>(function EntityTableRow({ rowId }, ref) { const columns = useRecoilScopedValue( visibleTableColumnsScopedSelector, TableRecoilScopeContext, @@ -24,6 +32,7 @@ export function EntityTableRow({ rowId }: { rowId: string }) { return ( ); -} +}); diff --git a/front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx index ccd015060..2e3da47b7 100644 --- a/front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx +++ b/front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx @@ -1,8 +1,12 @@ -import { useRef } from 'react'; +import { createContext, RefObject, useRef } from 'react'; import styled from '@emotion/styled'; import { useListenScroll } from '../hooks/useListenScroll'; +export const ScrollWrapperContext = createContext>({ + current: null, +}); + const StyledScrollWrapper = styled.div` display: flex; height: 100%; @@ -28,8 +32,10 @@ export function ScrollWrapper({ children, className }: ScrollWrapperProps) { }); return ( - - {children} - + + + {children} + + ); } diff --git a/front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts b/front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts new file mode 100644 index 000000000..fa8de7456 --- /dev/null +++ b/front/src/modules/ui/utilities/scroll/hooks/useScrollWrapperScopedRef.ts @@ -0,0 +1,14 @@ +import { useContext } from 'react'; + +import { ScrollWrapperContext } from '../components/ScrollWrapper'; + +export function useScrollWrapperScopedRef() { + const scrollWrapperRef = useContext(ScrollWrapperContext); + + if (!scrollWrapperRef) + throw new Error( + `Using a scoped ref without a ScrollWrapper : verify that you are using a ScrollWrapper if you intended to do so.`, + ); + + return scrollWrapperRef; +} diff --git a/front/yarn.lock b/front/yarn.lock index cdc60bb8c..3c790f24b 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1217,6 +1217,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.16.7": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.11.tgz#7a9ba3bbe406ad6f9e8dd4da2ece453eb23a77a4" + integrity sha512-ee7jVNlWN09+KftVOu9n7S8gQzD/Z6hN/I8VBRXW4P1+Xe7kJGXMwu8vds4aGIMHZnNbdpSWCfZZtinytpcAvA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -3633,6 +3640,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@reach/observe-rect@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" + integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== + "@remirror/core-constants@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a" @@ -4923,6 +4935,14 @@ resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.30.0.tgz#4ea3c4da56fd5653bb9d0be0dc7feaa33602555c" integrity sha512-tvtmkI4ALjKThVVORh++sB9JnkFY7eGInKxNy+Df7WVQiF7T85tlvGADzlgX4Ic+CK5MIUzZ0jhOlQ/RRlgXpg== +"@tanstack/react-virtual@^3.0.0-alpha.0": + version "3.0.0-alpha.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-alpha.0.tgz#2ca5a75fa609eca2b2ba622024d3aa3ee097bc30" + integrity sha512-WpHU/dt34NwZZ8qtiE05TF+nX/b1W6qrWZarO+s8jJFpPVicrTbJKp5Bjt4eSJuk7aYw272oEfsH3ABBRgj+3A== + dependencies: + "@babel/runtime" "^7.16.7" + "@reach/observe-rect" "^1.1.0" + "@testing-library/dom@>=7": version "9.3.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.1.tgz#8094f560e9389fb973fe957af41bf766937a9ee9"