From c03a1e177a0735ea15c010c3139ffacb56a2c234 Mon Sep 17 00:00:00 2001 From: Tim Ren <137012659+xr843@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:07:20 +0800 Subject: [PATCH] fix(web): clear announcement stagger timers on unmount The cleanup for the announcement-toast effect only cleared the outer 1500ms timer, leaving the per-announcement stagger timers (i * 800ms) untracked. If the root layout unmounts during the stagger window, those timers fire after cleanup and attempt to toast against a dead tree. Track the inner timer ids in an array and clear them alongside the outer timer in the effect cleanup. Fixes #1094. --- .../announcements/AnnouncementToastProvider.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/surfsense_web/components/announcements/AnnouncementToastProvider.tsx b/surfsense_web/components/announcements/AnnouncementToastProvider.tsx index 6cb1b17e5..ce2c9d172 100644 --- a/surfsense_web/components/announcements/AnnouncementToastProvider.tsx +++ b/surfsense_web/components/announcements/AnnouncementToastProvider.tsx @@ -65,7 +65,9 @@ export function AnnouncementToastProvider() { if (hasChecked.current) return; hasChecked.current = true; - const timer = setTimeout(() => { + const staggerTimers: ReturnType[] = []; + + const outerTimer = setTimeout(() => { const authed = isAuthenticated(); const active = getActiveAnnouncements(announcements, authed); const importantUntoasted = active.filter( @@ -74,11 +76,16 @@ export function AnnouncementToastProvider() { for (let i = 0; i < importantUntoasted.length; i++) { const announcement = importantUntoasted[i]; - setTimeout(() => showAnnouncementToast(announcement), i * 800); + staggerTimers.push( + setTimeout(() => showAnnouncementToast(announcement), i * 800) + ); } }, 1500); - return () => clearTimeout(timer); + return () => { + clearTimeout(outerTimer); + for (const id of staggerTimers) clearTimeout(id); + }; }, []); return null;