diff --git a/.changeset/long-months-laugh.md b/.changeset/long-months-laugh.md new file mode 100644 index 0000000000..c6c52abb2d --- /dev/null +++ b/.changeset/long-months-laugh.md @@ -0,0 +1,9 @@ +--- +'@backstage/ui': patch +--- + +Allow data to be passed directly to the `useTable` hook using the property `data` instead of `getData()` for mode `"complete"`. + +This simplifies usage as data changes, rather than having to perform a `useEffect` when data changes, and then reloading the data. It also happens immediately, so stale data won't remain until a rerender (with an internal async state change), so less flickering. + +Affected components: Table diff --git a/docs-ui/src/app/components/table/props-definition.tsx b/docs-ui/src/app/components/table/props-definition.tsx index e653f67c8a..be24e86bf0 100644 --- a/docs-ui/src/app/components/table/props-definition.tsx +++ b/docs-ui/src/app/components/table/props-definition.tsx @@ -36,7 +36,13 @@ export const useTableOptionsPropDefs: Record = { type: 'enum', values: ['function'], description: - 'Function that returns or fetches data (required). Signature varies by mode.', + 'Function that returns or fetches data (required for "offset" and "cursor" modes). For the "complete" mode, either this or `data` must be provided. Signature varies by mode.', + }, + data: { + type: 'enum', + values: ['T[]'], + description: + 'The data for the table. Only applicable for "complete" mode, and either this or `getData` must be provided.', }, paginationOptions: { type: 'enum', diff --git a/docs-ui/src/app/components/table/snippets.ts b/docs-ui/src/app/components/table/snippets.ts index cba479b59f..ab4bf53a58 100644 --- a/docs-ui/src/app/components/table/snippets.ts +++ b/docs-ui/src/app/components/table/snippets.ts @@ -12,7 +12,7 @@ const columns: ColumnConfig[] = [ function MyTable() { const { tableProps } = useTable({ mode: 'complete', - getData: () => data, + data, }); return ; @@ -39,7 +39,7 @@ const columns: ColumnConfig[] = [ function MyTable() { const { tableProps } = useTable({ mode: 'complete', - getData: () => data, + data, }); return
; @@ -69,7 +69,7 @@ export const tableSortingSnippet = `const columns: ColumnConfig[] = [ const { tableProps } = useTable({ mode: 'complete', - getData: () => data, + data, initialSort: { column: 'name', direction: 'ascending' }, sortFn: (items, { column, direction }) => { return [...items].sort((a, b) => { @@ -85,7 +85,7 @@ return
;`; export const tablePaginationSnippet = `const { tableProps } = useTable({ mode: 'complete', - getData: () => data, + data, paginationOptions: { pageSize: 10, pageSizeOptions: [10, 25, 50], @@ -94,7 +94,7 @@ export const tablePaginationSnippet = `const { tableProps } = useTable({ export const tableSearchSnippet = `const { tableProps, search } = useTable({ mode: 'complete', - getData: () => data, + data, searchFn: (items, query) => { const lowerQuery = query.toLowerCase(); return items.filter(item => @@ -119,7 +119,7 @@ export const tableSelectionSnippet = `const [selected, setSelected] = useState data, + data, }); return ( @@ -162,7 +162,7 @@ export const tableRowActionsDisabledSnippet = `
data, + data, searchFn: (items, query) => { /* ... */ }, }); diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 09ca9c6d96..82069bc469 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -2121,21 +2121,25 @@ export function useTable( ): UseTableResult; // @public (undocumented) -export interface UseTableCompleteOptions - extends QueryOptions { - // (undocumented) - filterFn?: (data: T[], filter: TFilter) => T[]; - // (undocumented) - getData: () => T[] | Promise; - // (undocumented) +export type UseTableCompleteOptions< + T extends TableItem, + TFilter = unknown, +> = QueryOptions & { mode: 'complete'; - // (undocumented) paginationOptions?: PaginationOptions; - // (undocumented) - searchFn?: (data: T[], search: string) => T[]; - // (undocumented) sortFn?: (data: T[], sort: SortDescriptor) => T[]; -} + filterFn?: (data: T[], filter: TFilter) => T[]; + searchFn?: (data: T[], search: string) => T[]; +} & ( + | { + data: T[] | undefined; + getData?: never; + } + | { + data?: never; + getData: () => T[] | Promise; + } + ); // @public (undocumented) export interface UseTableCursorOptions diff --git a/packages/ui/src/components/Table/hooks/types.ts b/packages/ui/src/components/Table/hooks/types.ts index f22efd4b53..163ec7741f 100644 --- a/packages/ui/src/components/Table/hooks/types.ts +++ b/packages/ui/src/components/Table/hooks/types.ts @@ -96,15 +96,25 @@ export interface CursorResponse { } /** @public */ -export interface UseTableCompleteOptions - extends QueryOptions { +export type UseTableCompleteOptions< + T extends TableItem, + TFilter = unknown, +> = QueryOptions & { mode: 'complete'; - getData: () => T[] | Promise; paginationOptions?: PaginationOptions; sortFn?: (data: T[], sort: SortDescriptor) => T[]; filterFn?: (data: T[], filter: TFilter) => T[]; searchFn?: (data: T[], search: string) => T[]; -} +} & ( + | { + data: T[] | undefined; + getData?: never; + } + | { + data?: never; + getData: () => T[] | Promise; + } + ); /** @public */ export interface UseTableOffsetOptions diff --git a/packages/ui/src/components/Table/hooks/useCompletePagination.ts b/packages/ui/src/components/Table/hooks/useCompletePagination.ts index 70db68dae1..5aae9a36c1 100644 --- a/packages/ui/src/components/Table/hooks/useCompletePagination.ts +++ b/packages/ui/src/components/Table/hooks/useCompletePagination.ts @@ -30,7 +30,8 @@ export function useCompletePagination( query: QueryState, ): PaginationResult & { reload: () => void } { const { - getData: getDataProp, + data, + getData: getDataProp = () => [], paginationOptions = {}, sortFn, filterFn, @@ -43,7 +44,7 @@ export function useCompletePagination( const { sort, filter, search } = query; const [items, setItems] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(!data); const [error, setError] = useState(undefined); const [loadCount, setLoadCount] = useState(0); @@ -52,6 +53,10 @@ export function useCompletePagination( // Load data on mount and when loadCount changes (reload trigger) useEffect(() => { + if (data) { + return; + } + let cancelled = false; setIsLoading(true); setError(undefined); @@ -75,7 +80,7 @@ export function useCompletePagination( return () => { cancelled = true; }; - }, [getData, loadCount]); + }, [data, getData, loadCount]); // Reset offset when query changes (query object is memoized) const prevQueryRef = useRef(query); @@ -86,9 +91,11 @@ export function useCompletePagination( } }, [query]); + const resolvedItems = useMemo(() => data ?? items, [data, items]); + // Process data client-side (filter, search, sort) const processedData = useMemo(() => { - let result = [...items]; + let result = [...resolvedItems]; if (filter !== undefined && filterFn) { result = filterFn(result, filter); } @@ -99,7 +106,7 @@ export function useCompletePagination( result = sortFn(result, sort); } return result; - }, [items, sort, filter, search, filterFn, searchFn, sortFn]); + }, [resolvedItems, sort, filter, search, filterFn, searchFn, sortFn]); const totalCount = processedData.length;