diff --git a/.changeset/unlucky-days-play.md b/.changeset/unlucky-days-play.md new file mode 100644 index 0000000000..b847ebd2fc --- /dev/null +++ b/.changeset/unlucky-days-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +Fix infinite loop in the notification title counter diff --git a/plugins/notifications/dev/index.tsx b/plugins/notifications/dev/index.tsx index b601c4b9a4..c35ac7250f 100644 --- a/plugins/notifications/dev/index.tsx +++ b/plugins/notifications/dev/index.tsx @@ -20,9 +20,11 @@ import { notificationsPlugin, NotificationsSidebarItem, } from '../src'; +import { signalsPlugin } from '@backstage/plugin-signals'; createDevApp() .registerPlugin(notificationsPlugin) + .registerPlugin(signalsPlugin) .addPage({ element: ( { if (!loading && !error && value) { setUnreadCount(value.unread); - if (titleCounterEnabled) { - setNotificationCount(value.unread); - } } - }, [loading, error, value, titleCounterEnabled, setNotificationCount]); + }, [loading, error, value]); + + useEffect(() => { + if (titleCounterEnabled) { + setNotificationCount(unreadCount); + } + }, [titleCounterEnabled, unreadCount, setNotificationCount]); // TODO: Figure out if the count can be added to hasNotifications return ( diff --git a/plugins/notifications/src/hooks/useTitleCounter.ts b/plugins/notifications/src/hooks/useTitleCounter.ts index d4ba22b946..d794678ea6 100644 --- a/plugins/notifications/src/hooks/useTitleCounter.ts +++ b/plugins/notifications/src/hooks/useTitleCounter.ts @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; /** @public */ export function useTitleCounter() { const [title, setTitle] = useState(document.title); const [count, setCount] = useState(0); + const titleTimer = useRef(undefined); const getPrefix = (value: number) => { return value === 0 ? '' : `(${value}) `; @@ -28,28 +29,43 @@ export function useTitleCounter() { return currentTitle.replace(/^\(\d+\)\s/, ''); }; - useEffect(() => { - document.title = title; - }, [title]); - useEffect(() => { const baseTitle = cleanTitle(title); - setTitle(`${getPrefix(count)}${baseTitle}`); + const shownTitle = `${getPrefix(count)}${baseTitle}`; + if (document.title !== shownTitle) { + window.clearTimeout(titleTimer.current); + document.title = shownTitle; + // Need to do this in timeout as the React Helmet overrides the title after this effect + titleTimer.current = window.setTimeout(() => { + document.title = shownTitle; + }, 50); + } return () => { + window.clearTimeout(titleTimer.current); document.title = cleanTitle(title); }; }, [title, count]); - const titleElement = document.querySelector('title'); - if (titleElement) { - new MutationObserver(() => { - setTitle(document.title); - }).observe(titleElement, { - subtree: true, - characterData: true, - childList: true, - }); - } + useEffect(() => { + const titleElement = document.querySelector('title'); + let observer: MutationObserver | undefined; + if (titleElement) { + observer = new MutationObserver(mutations => { + if (mutations?.[0]?.target?.textContent) { + setTitle(mutations[0].target.textContent); + } + }); + observer.observe(titleElement, { + characterData: true, + childList: true, + }); + } + return () => { + if (observer) { + observer.disconnect(); + } + }; + }, []); const setNotificationCount = useCallback( (newCount: number) => setCount(newCount), diff --git a/yarn.lock b/yarn.lock index af96b924bd..a07e9249f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6057,6 +6057,7 @@ __metadata: "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-notifications-common": "workspace:^" + "@backstage/plugin-signals": "workspace:^" "@backstage/plugin-signals-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^"