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
This commit is contained in:
Jérémy M
2023-09-04 13:33:02 +02:00
committed by GitHub
parent 9a35b1fa44
commit 3a0f02f2f2
7 changed files with 112 additions and 16 deletions

View File

@ -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) {

View File

@ -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<SpaceProps>`
${({ 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 (
<tbody>
{rowIds.map((rowId, index) => (
<RowIdContext.Provider value={rowId} key={rowId}>
<RowIndexContext.Provider value={index}>
<EntityTableRow rowId={rowId} />
</RowIndexContext.Provider>
</RowIdContext.Provider>
))}
</tbody>
<>
{paddingTop > 0 && (
<tr>
<StyledSpace top={paddingTop} />
</tr>
)}
{items.map((virtualItem) => {
const rowId = rowIds[virtualItem.index];
return (
<RowIdContext.Provider value={rowId} key={rowId}>
<RowIndexContext.Provider value={virtualItem.index}>
<EntityTableRow
key={virtualItem.index}
ref={virtualItem.measureRef}
rowId={rowId}
/>
</RowIndexContext.Provider>
</RowIdContext.Provider>
);
})}
{paddingBottom > 0 && (
<tr>
<StyledSpace bottom={paddingBottom} />
</tr>
)}
</>
);
}

View File

@ -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 (
<StyledRow
ref={ref}
data-testid={`row-id-${rowId}`}
selected={currentRowSelected}
data-selectable-id={rowId}
@ -41,4 +50,4 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
<td></td>
</StyledRow>
);
}
});

View File

@ -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<RefObject<HTMLDivElement>>({
current: null,
});
const StyledScrollWrapper = styled.div`
display: flex;
height: 100%;
@ -28,8 +32,10 @@ export function ScrollWrapper({ children, className }: ScrollWrapperProps) {
});
return (
<StyledScrollWrapper ref={scrollableRef} className={className}>
{children}
</StyledScrollWrapper>
<ScrollWrapperContext.Provider value={scrollableRef}>
<StyledScrollWrapper ref={scrollableRef} className={className}>
{children}
</StyledScrollWrapper>
</ScrollWrapperContext.Provider>
);
}

View File

@ -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;
}