diff --git a/.changeset/serious-chefs-provide.md b/.changeset/serious-chefs-provide.md new file mode 100644 index 0000000000..30d9adf15c --- /dev/null +++ b/.changeset/serious-chefs-provide.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +--- + +Avoid Layout Shift for `DismissableBanner` when using a `storageApi` with latency (e.g. `user-settings-backend`) + +Properly handle the `unknown` state of the `storageApi`. There's a trade-off: this may lead to some Layout Shift if the banner has not been dismissed, but once it has been dismissed, you won't have any. diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx index 8c18a1feee..f65db08393 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ReactNode, useState, useEffect } from 'react'; +import React, { ReactNode, useMemo } from 'react'; import { useApi, storageApiRef } from '@backstage/core-plugin-api'; import useObservable from 'react-use/esm/useObservable'; import classNames from 'classnames'; @@ -102,23 +102,17 @@ export const DismissableBanner = (props: Props) => { const classes = useStyles(); const storageApi = useApi(storageApiRef); const notificationsStore = storageApi.forBucket('notifications'); - const rawDismissedBanners = - notificationsStore.snapshot('dismissedBanners').value ?? []; - - const [dismissedBanners, setDismissedBanners] = useState( - new Set(rawDismissedBanners), - ); - const observedItems = useObservable( notificationsStore.observe$('dismissedBanners'), + notificationsStore.snapshot('dismissedBanners'), ); - useEffect(() => { - if (observedItems?.value) { - const currentValue = observedItems?.value ?? []; - setDismissedBanners(new Set(currentValue)); - } - }, [observedItems?.value]); + const dismissedBanners = useMemo( + () => new Set(observedItems.value ?? []), + [observedItems.value], + ); + + const loadingSettings = observedItems.presence === 'unknown'; const handleClick = () => { notificationsStore.set('dismissedBanners', [...dismissedBanners, id]); @@ -131,7 +125,7 @@ export const DismissableBanner = (props: Props) => { ? { vertical: 'bottom', horizontal: 'center' } : { vertical: 'top', horizontal: 'center' } } - open={!dismissedBanners.has(id)} + open={!loadingSettings && !dismissedBanners.has(id)} classes={{ root: classNames(classes.root, !fixed && classes.topPosition), }}