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/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index b15e5a34c6..6daea5ee34 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -97,11 +97,14 @@ export const NotificationsSidebarItem = (props?: { useEffect(() => { 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),