diff --git a/src/components/HighTable.tsx b/src/components/HighTable.tsx index b049552d..81f842a2 100644 --- a/src/components/HighTable.tsx +++ b/src/components/HighTable.tsx @@ -15,6 +15,7 @@ import { ViewportSizeProvider } from '../providers/ViewportSizeProvider.js' import type { HighTableProps } from '../types.js' import Scroller from './Scroller.js' import Slice from './Slice.js' +import Table from './Table.js' import Wrapper from './Wrapper.js' export default function HighTable({ data, ...props }: HighTableProps) { @@ -30,7 +31,7 @@ export default function HighTable({ data, ...props }: HighTableProps) { ) } -type StateProps = Pick +type StateProps = Pick & { children: ReactNode } function State({ @@ -42,6 +43,7 @@ function State({ focus, numRowsPerPage, orderBy, + overscan, padding, selection, onCellPositionChange, @@ -80,7 +82,7 @@ function State({ numRowsPerPage={numRowsPerPage} onCellPositionChange={onCellPositionChange} > - + {children} @@ -94,15 +96,13 @@ function State({ ) } -type DOMProps = Pick +type DOMProps = Pick function DOM({ className = '', maxRowNumber, - overscan, styled = true, onDoubleClickCell, - onError, onKeyDownCell, onMouseDownCell, renderCellContent, @@ -113,15 +113,15 @@ function DOM({
- + + + {/* puts a background behind the row labels column */} diff --git a/src/components/Scroller.tsx b/src/components/Scroller.tsx index 0fa40776..cd8c8b9b 100644 --- a/src/components/Scroller.tsx +++ b/src/components/Scroller.tsx @@ -2,7 +2,7 @@ import type { KeyboardEvent } from 'react' import { useCallback, useContext, useMemo } from 'react' import { CellNavigationContext } from '../contexts/CellNavigationContext.js' -import { ScrollContext } from '../contexts/ScrollContext.js' +import { CanvasHeightContext, SetScrollToContext, SetScrollTopContext } from '../contexts/ScrollContext.js' import { SetViewportSizeContext } from '../contexts/ViewportSizeContext.js' import styles from '../HighTable.module.css' @@ -14,8 +14,11 @@ interface Props { export default function Scroller({ children }: Props) { /** Callback to set the current viewport size */ const setViewportSize = useContext(SetViewportSizeContext) + // TODO(SL): get a stable function to go to current cell (maybe dispatch('ENTER_CELL_NAVIGATION_MODE'), or setMode('cells')) const { goToCurrentCell } = useContext(CellNavigationContext) - const { canvasHeight, sliceTop, setScrollTop, setScrollTo } = useContext(ScrollContext) + const setScrollTop = useContext(SetScrollTopContext) + const setScrollTo = useContext(SetScrollToContext) + const canvasHeight = useContext(CanvasHeightContext) /** * Handle keyboard events for scrolling @@ -92,16 +95,10 @@ export default function Scroller({ children }: Props) { return canvasHeight !== undefined ? { height: `${canvasHeight}px` } : {} }, [canvasHeight]) - const sliceTopStyle = useMemo(() => { - return sliceTop !== undefined ? { top: `${sliceTop}px` } : {} - }, [sliceTop]) - return (
-
- {children} -
+ {children}
) diff --git a/src/components/Slice.tsx b/src/components/Slice.tsx index 98038ded..cd44516b 100644 --- a/src/components/Slice.tsx +++ b/src/components/Slice.tsx @@ -1,246 +1,22 @@ -import type { KeyboardEvent } from 'react' -import { useCallback, useContext, useMemo } from 'react' +import { useContext, useMemo } from 'react' -import { CellNavigationContext } from '../contexts/CellNavigationContext.js' -import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js' -import { DataFrameMethodsContext, DataVersionContext, NumRowsContext } from '../contexts/DataContext.js' -import { OrderByContext } from '../contexts/OrderByContext.js' -import { ScrollContext } from '../contexts/ScrollContext.js' -import { SelectionContext } from '../contexts/SelectionContext.js' -import { ariaOffset } from '../helpers/constants.js' -import { useFetchCells } from '../hooks/useFetchCells.js' -import type { HighTableProps } from '../types.js' -import { stringify as stringifyDefault } from '../utils/stringify.js' -import Cell from './Cell.js' -import Row from './Row.js' -import RowHeader from './RowHeader.js' -import TableCorner from './TableCorner.js' -import TableHeader from './TableHeader.js' +import { SliceTopContext } from '../contexts/ScrollContext.js' -type SliceProps = Pick - -export default function Slice({ - overscan, - onDoubleClickCell, - onError, - onKeyDownCell, - onMouseDownCell, - renderCellContent, - stringify = stringifyDefault, -}: SliceProps) { - const { moveCell } = useContext(CellNavigationContext) - const orderBy = useContext(OrderByContext) - const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext) - const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext) - const { renderedRowsStart, renderedRowsEnd } = useContext(ScrollContext) - /** A version number that increments whenever a data frame is updated or resolved (the key remains the same). */ - const version = useContext(DataVersionContext) - /** The actual number of rows in the data frame */ - const numRows = useContext(NumRowsContext) - const dataFrameMethods = useContext(DataFrameMethodsContext) - - // Fetch the required cells if needed (visible + overscan) - // it's a side-effect. - useFetchCells({ overscan, onError }) - - const onNavigationTableKeyDown = useMemo(() => { - if (!moveCell) { - // disable keyboard navigation if moveCell is not provided - return - } - return (event: KeyboardEvent) => { - const { key, altKey, ctrlKey, metaKey, shiftKey } = event - // if the user is pressing Alt, Meta or Shift, do not handle the event - if (altKey || metaKey || shiftKey) { - return - } - if (key === 'ArrowRight') { - if (ctrlKey) { - moveCell({ type: 'LAST_COLUMN' }) - } else { - moveCell({ type: 'NEXT_COLUMN' }) - } - } else if (key === 'ArrowLeft') { - if (ctrlKey) { - moveCell({ type: 'FIRST_COLUMN' }) - } else { - moveCell({ type: 'PREVIOUS_COLUMN' }) - } - } else if (key === 'ArrowDown') { - if (ctrlKey) { - moveCell({ type: 'LAST_ROW' }) - } else { - moveCell({ type: 'NEXT_ROW' }) - } - } else if (key === 'ArrowUp') { - if (ctrlKey) { - moveCell({ type: 'FIRST_ROW' }) - } else { - moveCell({ type: 'PREVIOUS_ROW' }) - } - } else if (key === 'Home') { - if (ctrlKey) { - moveCell({ type: 'FIRST_CELL' }) - } else { - moveCell({ type: 'FIRST_COLUMN' }) - } - } else if (key === 'End') { - if (ctrlKey) { - moveCell({ type: 'LAST_CELL' }) - } else { - moveCell({ type: 'LAST_COLUMN' }) - } - } else if (key === 'PageDown') { - moveCell({ type: 'NEXT_ROWS_PAGE' }) - // TODO(SL): same for horizontal scrolling with Alt+PageDown? - } else if (key === 'PageUp') { - moveCell({ type: 'PREVIOUS_ROWS_PAGE' }) - // TODO(SL): same for horizontal scrolling with Alt+PageUp? - } else if (key !== ' ') { - // if the key is not one of the above, do not handle it - // special case: no action is associated with the Space key, but it's captured - // anyway to prevent the default action (scrolling the page) and stay in navigation mode - return - } - // avoid scrolling the table when the user is navigating with the keyboard - event.stopPropagation() - event.preventDefault() - } - }, [moveCell]) - - const onTableKeyDown = useMemo(() => { - if (onNavigationTableKeyDown || onSelectionTableKeyDown) { - return (event: KeyboardEvent) => { - onNavigationTableKeyDown?.(event) - onSelectionTableKeyDown?.(event) - } - } - }, [onNavigationTableKeyDown, onSelectionTableKeyDown]) - - const getOnCheckboxPress = useCallback(({ row, rowNumber }: { row: number, rowNumber?: number }) => { - if (rowNumber === undefined || !toggleRowNumber || !toggleRangeToRowNumber) { - return undefined - } - return ({ shiftKey }: { shiftKey: boolean }) => { - if (shiftKey) { - toggleRangeToRowNumber({ row, rowNumber }) - } else { - toggleRowNumber({ rowNumber }) - } - } - }, [toggleRowNumber, toggleRangeToRowNumber]) - - // Prepare the slice of data to render - // TODO(SL): also compute progress percentage here, to show a loading indicator - const slice = useMemo(() => { - if (renderedRowsStart === undefined || renderedRowsEnd === undefined) { - return { - rowContents: [], - canMeasureColumn: {}, - version, - } - } - const rows = Array.from({ length: renderedRowsEnd - renderedRowsStart }, (_, i) => renderedRowsStart + i) +interface Props { + /** Child components */ + children?: React.ReactNode +} - const canMeasureColumn: Record = {} - const rowContents = rows.map((row) => { - const rowNumber = dataFrameMethods.getRowNumber({ row, orderBy })?.value - const cells = (columnsParameters ?? []).map(({ name: column, index: originalColumnIndex, className }) => { - const cell = dataFrameMethods.getCell({ row, column, orderBy }) - canMeasureColumn[column] ||= cell !== undefined - return { columnIndex: originalColumnIndex, cell, className } - }) - return { - row, - rowNumber, - cells, - } - }) - return { - rowContents, - canMeasureColumn, - version, - } - }, [dataFrameMethods, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version]) +export default function Slice({ children }: Props) { + const sliceTop = useContext(SliceTopContext) - // don't render table if the data frame has no visible columns - // (it can have zero rows, but must have at least one visible column) - if (!columnsParameters) return + const sliceTopStyle = useMemo(() => { + return sliceTop !== undefined ? { top: `${sliceTop}px` } : {} + }, [sliceTop]) - const ariaColCount = columnsParameters.length + 1 // don't forget the selection column - const ariaRowCount = numRows + 1 // don't forget the header row return ( -
- - - - - - - - - {slice.rowContents.map(({ row, rowNumber, cells }) => { - const ariaRowIndex = row + ariaOffset - const selected = isRowSelected?.({ rowNumber }) - const rowKey = `${row}` - return ( - - - {cells.map(({ columnIndex, cell, className }, visibleColumnIndex) => { - return ( - - ) - })} - - ) - })} - -
+
+ {children} +
) } diff --git a/src/components/Table.tsx b/src/components/Table.tsx new file mode 100644 index 00000000..19f80910 --- /dev/null +++ b/src/components/Table.tsx @@ -0,0 +1,239 @@ +import type { KeyboardEvent } from 'react' +import { useCallback, useContext, useMemo } from 'react' + +import { CellNavigationContext } from '../contexts/CellNavigationContext.js' +import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js' +import { DataFrameMethodsContext, DataVersionContext, NumRowsContext } from '../contexts/DataContext.js' +import { OrderByContext } from '../contexts/OrderByContext.js' +import { RenderedRowsContext } from '../contexts/ScrollContext.js' +import { SelectionContext } from '../contexts/SelectionContext.js' +import { ariaOffset } from '../helpers/constants.js' +import type { HighTableProps } from '../types.js' +import { stringify as stringifyDefault } from '../utils/stringify.js' +import Cell from './Cell.js' +import Row from './Row.js' +import RowHeader from './RowHeader.js' +import TableCorner from './TableCorner.js' +import TableHeader from './TableHeader.js' + +type TableProps = Pick + +export default function Table({ + onDoubleClickCell, + onKeyDownCell, + onMouseDownCell, + renderCellContent, + stringify = stringifyDefault, +}: TableProps) { + const { moveCell } = useContext(CellNavigationContext) + const orderBy = useContext(OrderByContext) + const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext) + const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext) + const { renderedRowsStart, renderedRowsEnd } = useContext(RenderedRowsContext) + /** A version number that increments whenever a data frame is updated or resolved (the key remains the same). */ + const version = useContext(DataVersionContext) + /** The actual number of rows in the data frame */ + const numRows = useContext(NumRowsContext) + const dataFrameMethods = useContext(DataFrameMethodsContext) + + const onNavigationTableKeyDown = useMemo(() => { + if (!moveCell) { + // disable keyboard navigation if moveCell is not provided + return + } + return (event: KeyboardEvent) => { + const { key, altKey, ctrlKey, metaKey, shiftKey } = event + // if the user is pressing Alt, Meta or Shift, do not handle the event + if (altKey || metaKey || shiftKey) { + return + } + if (key === 'ArrowRight') { + if (ctrlKey) { + moveCell({ type: 'LAST_COLUMN' }) + } else { + moveCell({ type: 'NEXT_COLUMN' }) + } + } else if (key === 'ArrowLeft') { + if (ctrlKey) { + moveCell({ type: 'FIRST_COLUMN' }) + } else { + moveCell({ type: 'PREVIOUS_COLUMN' }) + } + } else if (key === 'ArrowDown') { + if (ctrlKey) { + moveCell({ type: 'LAST_ROW' }) + } else { + moveCell({ type: 'NEXT_ROW' }) + } + } else if (key === 'ArrowUp') { + if (ctrlKey) { + moveCell({ type: 'FIRST_ROW' }) + } else { + moveCell({ type: 'PREVIOUS_ROW' }) + } + } else if (key === 'Home') { + if (ctrlKey) { + moveCell({ type: 'FIRST_CELL' }) + } else { + moveCell({ type: 'FIRST_COLUMN' }) + } + } else if (key === 'End') { + if (ctrlKey) { + moveCell({ type: 'LAST_CELL' }) + } else { + moveCell({ type: 'LAST_COLUMN' }) + } + } else if (key === 'PageDown') { + moveCell({ type: 'NEXT_ROWS_PAGE' }) + // TODO(SL): same for horizontal scrolling with Alt+PageDown? + } else if (key === 'PageUp') { + moveCell({ type: 'PREVIOUS_ROWS_PAGE' }) + // TODO(SL): same for horizontal scrolling with Alt+PageUp? + } else if (key !== ' ') { + // if the key is not one of the above, do not handle it + // special case: no action is associated with the Space key, but it's captured + // anyway to prevent the default action (scrolling the page) and stay in navigation mode + return + } + // avoid scrolling the table when the user is navigating with the keyboard + event.stopPropagation() + event.preventDefault() + } + }, [moveCell]) + + const onTableKeyDown = useMemo(() => { + if (onNavigationTableKeyDown || onSelectionTableKeyDown) { + return (event: KeyboardEvent) => { + onNavigationTableKeyDown?.(event) + onSelectionTableKeyDown?.(event) + } + } + }, [onNavigationTableKeyDown, onSelectionTableKeyDown]) + + const getOnCheckboxPress = useCallback(({ row, rowNumber }: { row: number, rowNumber?: number }) => { + if (rowNumber === undefined || !toggleRowNumber || !toggleRangeToRowNumber) { + return undefined + } + return ({ shiftKey }: { shiftKey: boolean }) => { + if (shiftKey) { + toggleRangeToRowNumber({ row, rowNumber }) + } else { + toggleRowNumber({ rowNumber }) + } + } + }, [toggleRowNumber, toggleRangeToRowNumber]) + + // Prepare the slice of data to render + // TODO(SL): also compute progress percentage here, to show a loading indicator + const slice = useMemo(() => { + if (renderedRowsStart === undefined || renderedRowsEnd === undefined) { + return { + rowContents: [], + canMeasureColumn: {}, + version, + } + } + const rows = Array.from({ length: renderedRowsEnd - renderedRowsStart }, (_, i) => renderedRowsStart + i) + + const canMeasureColumn: Record = {} + const rowContents = rows.map((row) => { + const rowNumber = dataFrameMethods.getRowNumber({ row, orderBy })?.value + const cells = (columnsParameters ?? []).map(({ name: column, index: originalColumnIndex, className }) => { + const cell = dataFrameMethods.getCell({ row, column, orderBy }) + canMeasureColumn[column] ||= cell !== undefined + return { columnIndex: originalColumnIndex, cell, className } + }) + return { + row, + rowNumber, + cells, + } + }) + return { + rowContents, + canMeasureColumn, + version, + } + }, [dataFrameMethods, columnsParameters, renderedRowsStart, renderedRowsEnd, orderBy, version]) + + // don't render table if the data frame has no visible columns + // (it can have zero rows, but must have at least one visible column) + if (!columnsParameters) return + + const ariaColCount = columnsParameters.length + 1 // don't forget the selection column + const ariaRowCount = numRows + 1 // don't forget the header row + return ( + + + + + + + + + + {slice.rowContents.map(({ row, rowNumber, cells }) => { + const ariaRowIndex = row + ariaOffset + const selected = isRowSelected?.({ rowNumber }) + const rowKey = `${row}` + return ( + + + {cells.map(({ columnIndex, cell, className }, visibleColumnIndex) => { + return ( + + ) + })} + + ) + })} + +
+ ) +} diff --git a/src/contexts/ScrollContext.ts b/src/contexts/ScrollContext.ts index cf2e126b..a9332ffc 100644 --- a/src/contexts/ScrollContext.ts +++ b/src/contexts/ScrollContext.ts @@ -1,32 +1,29 @@ +import type { Dispatch, SetStateAction } from 'react' import { createContext } from 'react' -export interface ScrollContextType { - /** Total scrollable height, in pixels */ - canvasHeight?: number - /** Offset of the top of the visible slice from the top of the canvas, in pixels */ - sliceTop?: number - /** Index of the first row visible in the viewport (inclusive). Indexes refer to the virtual table domain. */ - visibleRowsStart?: number - /** Index of the last row visible in the viewport (exclusive). */ - visibleRowsEnd?: number +/** + * Function to call when the current scroll top position changes (on scroll) + * + * @param scrollTop The new scroll top position in pixels + */ +export const SetScrollTopContext = createContext<((scrollTop: number) => void) | undefined>(undefined) + +/** + * Function to set the scrollTo function + * + * @param scrollTo The scrollTo function of the viewport element (on component mount), or undefined (on unmount) + */ +export const SetScrollToContext = createContext> | undefined>(undefined) + +/** Total scrollable height, in pixels */ +export const CanvasHeightContext = createContext(undefined) + +/** Offset of the top of the visible slice from the top of the canvas, in pixels */ +export const SliceTopContext = createContext(undefined) + +export const RenderedRowsContext = createContext<{ /** Index of the first row rendered in the DOM as a table row (inclusive). */ renderedRowsStart?: number /** Index of the last row rendered in the DOM as a table row (exclusive). */ renderedRowsEnd?: number - /** - * Function to set the scrollTo function - * - * @param scrollTo The scrollTo function of the viewport element (on component mount), or undefined (on unmount) - */ - setScrollTo?: (scrollTo: HTMLElement['scrollTo'] | undefined) => void - /** - * Function to call when the current scroll top position changes (on scroll) - * - * @param scrollTop The new scroll top position in pixels - */ - setScrollTop?: (scrollTop: number) => void -} - -export const defaultScrollContext: ScrollContextType = {} - -export const ScrollContext = createContext(defaultScrollContext) +}>({}) diff --git a/src/hooks/useFetchCells.ts b/src/hooks/useFetchCells.ts index 0d314046..7e0716f2 100644 --- a/src/hooks/useFetchCells.ts +++ b/src/hooks/useFetchCells.ts @@ -3,31 +3,30 @@ import { useContext, useEffect, useEffectEvent, useMemo } from 'react' import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js' import { DataFrameMethodsContext, NumRowsContext } from '../contexts/DataContext.js' import { OrderByContext } from '../contexts/OrderByContext.js' -import { ScrollContext } from '../contexts/ScrollContext.js' import { defaultOverscan } from '../helpers/constants.js' import type { HighTableProps } from '../types.js' -type Props = Pick +type Props = Pick & { + range?: { + /** Index of the first row visible in the viewport (inclusive). Indexes refer to the virtual table domain. */ + visibleRowsStart?: number + /** Index of the last row visible in the viewport (exclusive). */ + visibleRowsEnd?: number + } +} /** * Fetch the required cells (visible + overscan). */ -export function useFetchCells({ overscan = defaultOverscan, onError }: Props) { - const { visibleRowsStart, visibleRowsEnd } = useContext(ScrollContext) +export function useFetchCells({ overscan = defaultOverscan, range = {}, onError }: Props) { const { visibleColumnsParameters } = useContext(ColumnsVisibilityContext) const orderBy = useContext(OrderByContext) const dataFrameMethods = useContext(DataFrameMethodsContext) const numRows = useContext(NumRowsContext) + const { visibleRowsStart, visibleRowsEnd } = range - const fetchedRowsStart = useMemo(() => { - if (visibleRowsStart === undefined) return undefined - return Math.max(0, visibleRowsStart - overscan) - }, [visibleRowsStart, overscan]) - - const fetchedRowsEnd = useMemo(() => { - if (visibleRowsEnd === undefined) return undefined - return Math.min(numRows, visibleRowsEnd + overscan) - }, [visibleRowsEnd, numRows, overscan]) + const fetchedRowsStart = visibleRowsStart === undefined ? undefined : Math.max(0, visibleRowsStart - overscan) + const fetchedRowsEnd = visibleRowsEnd === undefined ? undefined : Math.min(numRows, visibleRowsEnd + overscan) const columnNames = useMemo(() => { return (visibleColumnsParameters ?? []).map(({ name }) => name) diff --git a/src/providers/ScrollProvider.tsx b/src/providers/ScrollProvider.tsx index 60e78b2b..db981154 100644 --- a/src/providers/ScrollProvider.tsx +++ b/src/providers/ScrollProvider.tsx @@ -2,22 +2,23 @@ import { type ReactNode, useCallback, useContext, useEffect, useMemo, useReducer import { CellNavigationContext } from '../contexts/CellNavigationContext.js' import { NumRowsContext } from '../contexts/DataContext.js' -import { ScrollContext } from '../contexts/ScrollContext.js' +import { CanvasHeightContext, RenderedRowsContext, SetScrollToContext, SetScrollTopContext, SliceTopContext } from '../contexts/ScrollContext.js' import { TableCornerHeightContext } from '../contexts/TableCornerSizeContext.js' import { ViewportHeightContext } from '../contexts/ViewportSizeContext.js' import { defaultPadding, maxElementHeight, rowHeight } from '../helpers/constants.js' import { computeDerivedValues, createScale, getScrollActionForRow, initializeScrollState, scrollReducer } from '../helpers/scroll.js' +import { useFetchCells } from '../hooks/useFetchCells.js' import type { HighTableProps } from '../types.js' -type ScrollProviderProps = Pick & { +type ScrollProviderProps = Pick & { /** Child components */ children: ReactNode } /** - * Provide the scroll state and logic to the table, through the ScrollContext. + * Provide the scroll state and logic to the table, through the ScrollContext contexts. */ -export function ScrollProvider({ children, padding = defaultPadding }: ScrollProviderProps) { +export function ScrollProvider({ children, overscan, padding = defaultPadding, onError }: ScrollProviderProps) { const [{ scale, scrollTop, scrollTopAnchor, localOffset }, dispatch] = useReducer(scrollReducer, undefined, initializeScrollState) const { cellPosition, focusState, focusDispatch } = useContext(CellNavigationContext) const clientHeight = useContext(ViewportHeightContext) @@ -82,24 +83,40 @@ export function ScrollProvider({ children, padding = defaultPadding }: ScrollPro } }, [cellPosition, scrollTo, scrollTopAnchor, localOffset, scale, focusDispatch, focusState]) - const value = useMemo(() => { + const derivedValues = useMemo(() => { + if (!scale) { + return undefined + } + return computeDerivedValues({ + scale, + scrollTop, + scrollTopAnchor, + localOffset, + padding }) + }, [scale, scrollTop, scrollTopAnchor, localOffset, padding]) + + const renderedRows = useMemo(() => { return { - scrollMode: 'virtual' as const, - canvasHeight: scale ? scale.canvasHeight : undefined, - setScrollTop, - setScrollTo, - ...computeDerivedValues({ - scale, - scrollTop, - scrollTopAnchor, - localOffset, - padding, - }), + renderedRowsStart: derivedValues?.renderedRowsStart, + renderedRowsEnd: derivedValues?.renderedRowsEnd, } - }, [scale, scrollTop, scrollTopAnchor, localOffset, padding, setScrollTop]) + }, [derivedValues]) + + // Fetch the required cells if needed (visible + overscan) + // it's a side-effect. + useFetchCells({ overscan, onError, range: derivedValues }) + return ( - - {children} - + + + + + + {children} + + + + + ) }