From 51aacae34b398fed4b7de6f4a6c3efcbcdde5587 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Mar 2026 09:49:05 +0100 Subject: [PATCH 1/6] Fix catalog filter flicker caused by stale render cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The EntityListProvider exposed entities from `outputState` (synced via useEffect) but `loading` from `useAsyncFn`. When the async function resolved, `loading` flipped to false one render before `outputState` updated, causing a flash of stale data between the loading indicator and the new results. Use `resolvedValue` directly in the context value when available, eliminating the extra render lag. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .changeset/fix-catalog-filter-flicker.md | 5 +++++ .../src/hooks/useEntityListProvider.tsx | 22 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-catalog-filter-flicker.md diff --git a/.changeset/fix-catalog-filter-flicker.md b/.changeset/fix-catalog-filter-flicker.md new file mode 100644 index 0000000000..5964f7ada9 --- /dev/null +++ b/.changeset/fix-catalog-filter-flicker.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixed a UI flicker in the catalog entity list where changing a filter would briefly flash stale data before showing the new results. diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index 12eb9dff6c..a18575befa 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -454,30 +454,36 @@ export const EntityListProvider = ( [paginationMode], ); + // Use resolvedValue directly when available to avoid an extra render cycle. + // Without this, there's a render where loading has flipped back to false but + // outputState hasn't been updated yet (it syncs via useEffect), causing a + // flash of stale data between the loading state and the new results. + const latestOutput = resolvedValue ?? outputState; + const pageInfo = useMemo(() => { if (paginationMode !== 'cursor') { return undefined; } - const prevCursor = outputState.pageInfo?.prevCursor; - const nextCursor = outputState.pageInfo?.nextCursor; + const prevCursor = latestOutput.pageInfo?.prevCursor; + const nextCursor = latestOutput.pageInfo?.nextCursor; return { prev: prevCursor ? () => setCursor(prevCursor) : undefined, next: nextCursor ? () => setCursor(nextCursor) : undefined, }; - }, [paginationMode, outputState.pageInfo]); + }, [paginationMode, latestOutput.pageInfo]); const value = useMemo( () => ({ - filters: outputState.appliedFilters, - entities: outputState.entities, - backendEntities: outputState.backendEntities, + filters: latestOutput.appliedFilters, + entities: latestOutput.entities, + backendEntities: latestOutput.backendEntities, updateFilters, queryParameters, loading, error, pageInfo, - totalItems: outputState.totalItems, + totalItems: latestOutput.totalItems, limit, offset, setLimit, @@ -485,7 +491,7 @@ export const EntityListProvider = ( paginationMode, }), [ - outputState, + latestOutput, updateFilters, queryParameters, loading, From 744f9044a274460fe83e9961cc687f0714dee374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Mar 2026 10:06:14 +0100 Subject: [PATCH 2/6] Keep showing stale table data during filter loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Table component replaces all data rows with a loading spinner when isLoading is true. This caused a flash of empty table between filter changes. Now only show the loading state when there's no data to display yet (initial load), keeping stale results visible during filter transitions. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .changeset/fix-catalog-table-loading-flash.md | 5 +++++ .../src/components/CatalogTable/CatalogTable.tsx | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-catalog-table-loading-flash.md diff --git a/.changeset/fix-catalog-table-loading-flash.md b/.changeset/fix-catalog-table-loading-flash.md new file mode 100644 index 0000000000..1e75a6d7af --- /dev/null +++ b/.changeset/fix-catalog-table-loading-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed the catalog table briefly showing an empty loading state when changing filters. The table now keeps displaying stale results until new data arrives. diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 7875431246..165d75bf48 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -108,6 +108,12 @@ export const CatalogTable = (props: CatalogTableProps) => { paginationMode, } = entityListContext; + // Only show the full loading indicator when there's no data to display yet + // (i.e. initial load). During filter changes we already have stale data to + // show, so we keep it visible and let the new results swap in seamlessly + // instead of briefly flashing an empty table. + const isLoading = loading && entities.length === 0; + const tableColumns = useMemo( () => typeof columns === 'function' ? columns(entityListContext) : columns, @@ -199,7 +205,7 @@ export const CatalogTable = (props: CatalogTableProps) => { const options: TableProps['options'] = { actionsColumnIndex: -1, loadingType: 'linear' as const, - showEmptyDataSourceMessage: !loading, + showEmptyDataSourceMessage: !isLoading, padding: 'dense' as const, ...tableOptions, }; @@ -209,7 +215,7 @@ export const CatalogTable = (props: CatalogTableProps) => { { { return ( - isLoading={loading} + isLoading={isLoading} columns={tableColumns} options={{ paging: showPagination, From 7db2f073225b8abec467b078850dd5b0284c4778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Mar 2026 10:11:03 +0100 Subject: [PATCH 3/6] Show inline spinner in table title during filter refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When filters change and a fetch is in progress but stale data is still displayed, show a small circular spinner after the title text (e.g. "Owned Components (5) ⟳") to indicate that updated results are being loaded. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/components/CatalogTable/CatalogTable.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 165d75bf48..1a9d363ec4 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -34,6 +34,7 @@ import { useEntityList, useStarredEntities, } from '@backstage/plugin-catalog-react'; +import CircularProgress from '@material-ui/core/CircularProgress'; import Typography from '@material-ui/core/Typography'; import { visuallyHidden } from '@mui/utils'; import Edit from '@material-ui/icons/Edit'; @@ -195,11 +196,20 @@ export const CatalogTable = (props: CatalogTableProps) => { const titlePreamble = capitalize( filters.user?.value ?? t('catalogTable.allFilters'), ); - const title = + const titleText = props.title || [titlePreamble, currentType, pluralize(currentKind), currentCount] .filter(s => s) .join(' '); + const title = + loading && !isLoading ? ( + <> + {titleText}{' '} + + + ) : ( + titleText + ); const actions = props.actions || defaultActions; const options: TableProps['options'] = { From eb30aafd2c21fab080ac08f1528c5f32f9567748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Mar 2026 10:13:32 +0100 Subject: [PATCH 4/6] Align loading spinner with title text and reduce size MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the title + spinner in an inline-flex container for vertical centering, and reduce the spinner to 0.8em so it doesn't affect the overall box height. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../catalog/src/components/CatalogTable/CatalogTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 1a9d363ec4..9a667c76be 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -203,10 +203,10 @@ export const CatalogTable = (props: CatalogTableProps) => { .join(' '); const title = loading && !isLoading ? ( - <> + {titleText}{' '} - - + + ) : ( titleText ); From e073aaa9ac6ba915a29f2b4502198463a61bc2dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Mar 2026 10:14:13 +0100 Subject: [PATCH 5/6] Add gap between title text and loading spinner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../catalog/src/components/CatalogTable/CatalogTable.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 9a667c76be..093c92c1a0 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -203,8 +203,10 @@ export const CatalogTable = (props: CatalogTableProps) => { .join(' '); const title = loading && !isLoading ? ( - - {titleText}{' '} + + {titleText} ) : ( From cf17daca99ba66c298af9b20967364c678e18e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 19 Mar 2026 10:37:34 +0100 Subject: [PATCH 6/6] Only suppress loading for non-paginated tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For paginated tables, always show the loading indicator since stale data from a different page would be misleading. The loading suppression (showing stale data during refresh) only applies to the non-paginated table where the stale filtered results are still meaningful. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/components/CatalogTable/CatalogTable.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 093c92c1a0..80b37002c5 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -109,11 +109,13 @@ export const CatalogTable = (props: CatalogTableProps) => { paginationMode, } = entityListContext; - // Only show the full loading indicator when there's no data to display yet - // (i.e. initial load). During filter changes we already have stale data to - // show, so we keep it visible and let the new results swap in seamlessly - // instead of briefly flashing an empty table. - const isLoading = loading && entities.length === 0; + // For non-paginated tables, only show the full loading indicator when + // there's no data yet (initial load). During filter changes we keep stale + // data visible and let the new results swap in seamlessly. For paginated + // tables we always show loading, since stale data from a different page + // would be misleading. + const isLoading = + paginationMode === 'none' ? loading && entities.length === 0 : loading; const tableColumns = useMemo( () =>