diff --git a/packages/jsapi-components/src/useInitializeViewportData.ts b/packages/jsapi-components/src/useInitializeViewportData.ts index fe28c0fd35..b6a8c3114f 100644 --- a/packages/jsapi-components/src/useInitializeViewportData.ts +++ b/packages/jsapi-components/src/useInitializeViewportData.ts @@ -8,6 +8,50 @@ import useTableSize from './useTableSize'; const log = Log.module('useInitializeViewportData'); +/** + * Given an array of items, returns a new array containing the target number of + * items. If reuseExistingItems is true, existing items will be re-used. If + * false, all items will be replaced with new, empty items. + * @param items The array of items to resize. + * @param targetSize The desired size of the array. + * @param reuseExistingItems If true, existing items will be re-used. If false, + * all items will be replaced with new, empty items. + * @returns The resized array of items. + */ +function resizeItemsArray({ + items, + targetSize, + reuseExistingItems, +}: { + items: KeyedItem[]; + reuseExistingItems: boolean; + targetSize: number; +}): KeyedItem[] { + const currentSize = items.length; + + // If size isn't changing, do nothing + if (currentSize === targetSize) { + return items; + } + + log.debug('size changed:', { currentSize, targetSize }); + + if (!reuseExistingItems) { + // All items will be replaced with new data. This is preferred in certain + // scenarios to avoid the user seeing items shift around multiple times + // while data is being loaded. + return Array.from(generateEmptyKeyedItems(0, targetSize - 1)); + } + + // Drop extra items + if (currentSize > targetSize) { + return items.slice(0, targetSize - 1); + } + + // Add missing items + return [...items, ...generateEmptyKeyedItems(currentSize, targetSize - 1)]; +} + /** * Initializes a ListData instance that can be used for windowed views of a * Table. The list must always contain a KeyedItem for every record in the table, @@ -18,28 +62,29 @@ const log = Log.module('useInitializeViewportData'); * source table. This is intended for "human" sized tables such as those used in * admin panels. This is not suitable for "machine" scale with millions+ rows. * @param table The table that will be used to determine the list size. + * @param reuseItemsOnTableResize If true, the items will be reused when the + * table resizes. Defaults to false which will replace the items when the table + * resizes. * @returns a WindowedListData object. */ export function useInitializeViewportData( - table: dh.Table | dh.TreeTable | null + table: dh.Table | dh.TreeTable | null, + reuseItemsOnTableResize = false ): WindowedListData> { const viewportData = useWindowedListData>({}); // If the table changes size, we need to re-initialize it. const targetSize = Math.max(0, useTableSize(table)); - // Whenever the table reference or size changes, replace the list with empty - // items. This is preferred over updating items in place to avoid the user - // seeing items shift around multiple times. + // Whenever the table reference or size changes, resize the list. useEffect(() => { - const currentSize = viewportData.items.length; - - if (targetSize !== currentSize) { - log.debug('size changed:', { currentSize, targetSize }); - viewportData.setItems( - Array.from(generateEmptyKeyedItems(0, targetSize - 1)) - ); - } + viewportData.setItems( + resizeItemsArray({ + items: viewportData.items, + targetSize, + reuseExistingItems: reuseItemsOnTableResize, + }) + ); // Intentionally excluding viewportData since it changes on every render. // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/jsapi-components/src/useViewportData.ts b/packages/jsapi-components/src/useViewportData.ts index 71af52e2ed..fdbbce70f7 100644 --- a/packages/jsapi-components/src/useViewportData.ts +++ b/packages/jsapi-components/src/useViewportData.ts @@ -24,6 +24,7 @@ export interface UseViewportDataProps< TItem, TTable extends dh.Table | dh.TreeTable, > { + reuseItemsOnTableResize?: boolean; table: TTable | null; itemHeight?: number; scrollDebounce?: number; @@ -64,6 +65,8 @@ export interface UseViewportDataResult< * @param viewportSize The number of items to display in the viewport. * @param viewportPadding The number of items to fetch at start and end of the viewport. * @param deserializeRow A function to deserialize a row from the Table. + * @param reuseItemsOnTableResize If true, existing items will be re-used when + * the table size changes. * @returns An object for managing Table viewport state. */ export function useViewportData({ @@ -73,8 +76,12 @@ export function useViewportData({ viewportSize = 10, viewportPadding = 50, deserializeRow = defaultRowDeserializer, + reuseItemsOnTableResize = false, }: UseViewportDataProps): UseViewportDataResult { - const viewportData = useInitializeViewportData(table); + const viewportData = useInitializeViewportData( + table, + reuseItemsOnTableResize + ); const setPaddedViewport = useSetPaddedViewportCallback( table,