diff --git a/apps/studio/components/ui/AdvisorPanel/useAdvisorSignals.ts b/apps/studio/components/ui/AdvisorPanel/useAdvisorSignals.ts index d5ea84f9e42..08592d73b24 100644 --- a/apps/studio/components/ui/AdvisorPanel/useAdvisorSignals.ts +++ b/apps/studio/components/ui/AdvisorPanel/useAdvisorSignals.ts @@ -72,19 +72,25 @@ export const useAdvisorSignals = ({ projectRef, enabled = true }: UseAdvisorSign [projectRef, data] ) - // Prune stale dismissals when the active signal list changes (e.g. an IP was unbanned) + // Prune stale dismissals when the active signal list changes (e.g. an IP was unbanned). + // Only call the setter when pruning would actually change something — otherwise we + // churn subscribers unnecessarily, which can cause feedback loops when this hook is + // mounted in more than one place (AdvisorSection + AdvisorPanel). useEffect(() => { if (!data) return - const activeKeys = new Set(signalItems.map((item) => item.dismissalKey)) + const hasStaleBannedIPDismissal = dismissedKeys.some( + (key) => + key.startsWith('signal:banned-ip:') && + !signalItems.some((item) => item.dismissalKey === key) + ) + if (!hasStaleBannedIPDismissal) return - setDismissedKeys((current) => { - const next = current.filter((key) => - key.startsWith('signal:banned-ip:') ? activeKeys.has(key) : true - ) - return next.length === current.length ? current : next - }) - }, [data, signalItems, setDismissedKeys]) + const activeKeys = new Set(signalItems.map((item) => item.dismissalKey)) + setDismissedKeys((current) => + current.filter((key) => (key.startsWith('signal:banned-ip:') ? activeKeys.has(key) : true)) + ) + }, [data, signalItems, dismissedKeys, setDismissedKeys]) const formattedData = useMemo( () => signalItems.filter((item) => !dismissedKeySet.has(item.dismissalKey)), diff --git a/apps/studio/hooks/misc/useLocalStorage.ts b/apps/studio/hooks/misc/useLocalStorage.ts index f9fa942195a..73bce8d7c58 100644 --- a/apps/studio/hooks/misc/useLocalStorage.ts +++ b/apps/studio/hooks/misc/useLocalStorage.ts @@ -86,6 +86,14 @@ export function useLocalStorageQuery(key: string, initialValue: T) { const currentValue = queryClient.getQueryData(queryKey) ?? initialValue const valueToStore = value instanceof Function ? value(currentValue) : value + // Bail out when the value is unchanged (matches useState semantics). + // Without this, no-op updates from consumers — like a pruning effect + // whose updater returns `current` unchanged — still write to + // localStorage and invalidate the query, which churns subscribers and + // can cascade into "Maximum update depth exceeded" when two consumers + // of the same key are mounted together. + if (Object.is(valueToStore, currentValue)) return + if (typeof window !== 'undefined') { window.localStorage.setItem(key, JSON.stringify(valueToStore)) }