Merge pull request #33432 from backstage/freben/fix-catalog-filter-flicker

Fix catalog filter flicker caused by stale render cycle
This commit is contained in:
Fredrik Adelöw
2026-03-19 11:01:48 +01:00
committed by GitHub
4 changed files with 49 additions and 13 deletions
+5
View File
@@ -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.
@@ -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.
@@ -454,30 +454,36 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
[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 = <EntityFilters extends DefaultEntityFilters>(
paginationMode,
}),
[
outputState,
latestOutput,
updateFilters,
queryParameters,
loading,
@@ -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';
@@ -108,6 +109,14 @@ export const CatalogTable = (props: CatalogTableProps) => {
paginationMode,
} = entityListContext;
// 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(
() =>
typeof columns === 'function' ? columns(entityListContext) : columns,
@@ -189,17 +198,28 @@ 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 ? (
<span
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5em' }}
>
{titleText}
<CircularProgress size="0.8em" data-testid="loading-indicator" />
</span>
) : (
titleText
);
const actions = props.actions || defaultActions;
const options: TableProps['options'] = {
actionsColumnIndex: -1,
loadingType: 'linear' as const,
showEmptyDataSourceMessage: !loading,
showEmptyDataSourceMessage: !isLoading,
padding: 'dense' as const,
...tableOptions,
};
@@ -209,7 +229,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
<CursorPaginatedCatalogTable
columns={tableColumns}
emptyContent={emptyContent}
isLoading={loading}
isLoading={isLoading}
title={title}
actions={actions}
subtitle={subtitle}
@@ -224,7 +244,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
<OffsetPaginatedCatalogTable
columns={tableColumns}
emptyContent={emptyContent}
isLoading={loading}
isLoading={isLoading}
title={title}
actions={actions}
subtitle={subtitle}
@@ -240,7 +260,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
return (
<Table<CatalogTableRow>
isLoading={loading}
isLoading={isLoading}
columns={tableColumns}
options={{
paging: showPagination,