Skip to content

Commit d2d2156

Browse files
fix(emcn): sweep route-scoped toasts during render, not after child effects
The provider cleared route-scoped toasts from a pathname effect. Effects run child-first, so it also swept toasts the newly rendered route had just raised: any toast added from a child's mount effect — which is what happens whenever that child's data is already cached — was appended and filtered out in the same commit, before it painted. Move the sweep into render, where it runs before children render, so only toasts predating the navigation are cleared. Timers and heights were already reconciled by effects keyed off `toasts`, so this stays a pure state update. Drops the persistAcrossRoutes workaround the table lock notice needed to dodge the bug; its tableId-keyed cleanup stays, covering embedded swaps that change the prop without a route change.
1 parent 072f658 commit d2d2156

2 files changed

Lines changed: 26 additions & 35 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]
  • packages/emcn/src/components/toast

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -627,11 +627,6 @@ export function Table({
627627
const notify = action === 'status' ? toast.info : toast.warning
628628
blockedToastIdRef.current = notify(title, {
629629
description: text,
630-
// The provider's route-change sweep runs after child effects, so an
631-
// announcement fired on a warm-cache navigation would be cleared in the
632-
// same commit it was added. These toasts belong to this view, so they
633-
// opt out of the sweep and are dismissed on unmount instead.
634-
persistAcrossRoutes: true,
635630
...(canOpenLockSettings
636631
? {
637632
action: { label: 'Lock settings', onClick: () => setShowLockSettings(true) },
@@ -656,10 +651,9 @@ export function Table({
656651
showBlockedToast('status')
657652
}, [tableData, userPermissions.isLoading, showBlockedToast])
658653

659-
// Counterpart to `persistAcrossRoutes` above: a notice must not outlive the
660-
// table it describes. Keyed on `tableId` because switching tables reuses this
661-
// component instead of remounting it, and the action would then target the
662-
// table the user just moved to.
654+
// A notice must not outlive the table it describes — its action targets
655+
// whichever table is current. Keyed on `tableId` so an embedded swap that
656+
// changes the prop without a route change is covered too.
663657
useEffect(
664658
() => () => {
665659
if (!blockedToastIdRef.current) return

packages/emcn/src/components/toast/toast.tsx

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,29 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
396396
const [mounted, setMounted] = useState(false)
397397
const timersRef = useRef(new Map<string, ReturnType<typeof setTimeout>>())
398398

399+
/**
400+
* Clear the previous route's toasts when the route changes. Toasts flagged
401+
* `persistAcrossRoutes` — global, ongoing-state notifications like the
402+
* connection status — survive; page-scoped ones do not.
403+
*
404+
* Done during render rather than in an effect. Effects run child-first, so an
405+
* effect here would also sweep toasts the newly rendered route just raised: a
406+
* toast added from a child's mount effect — which is what happens whenever
407+
* that child's data is already cached — would be appended and filtered out in
408+
* the same commit, before it ever painted. Adjusting state during render runs
409+
* before children render, so only toasts predating the navigation are cleared.
410+
*
411+
* Stays a pure state update: orphaned timers and measured heights are already
412+
* reconciled by the effects below, which key off `toasts`.
413+
*/
414+
const [sweptPathname, setSweptPathname] = useState(pathname)
415+
if (pathname !== sweptPathname) {
416+
setSweptPathname(pathname)
417+
setToasts((prev) =>
418+
prev.some((t) => !t.persistAcrossRoutes) ? prev.filter((t) => t.persistAcrossRoutes) : prev
419+
)
420+
}
421+
399422
useEffect(() => {
400423
setMounted(true)
401424
}, [])
@@ -459,27 +482,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
459482
setHeights({})
460483
}, [])
461484

462-
/**
463-
* Clear only route-scoped toasts. Toasts flagged `persistAcrossRoutes` —
464-
* global, ongoing-state notifications like the connection status — survive,
465-
* everything else (page-scoped notifications) is cleared on navigation.
466-
*/
467-
const dismissRouteScopedToasts = useCallback(() => {
468-
setToasts((prev) => {
469-
const kept = prev.filter((t) => t.persistAcrossRoutes)
470-
if (kept.length === prev.length) return prev
471-
for (const t of prev) {
472-
if (t.persistAcrossRoutes) continue
473-
const timer = timersRef.current.get(t.id)
474-
if (timer) {
475-
clearTimeout(timer)
476-
timersRef.current.delete(t.id)
477-
}
478-
}
479-
return kept
480-
})
481-
}, [])
482-
483485
const measureToast = useCallback((id: string, height: number) => {
484486
setHeights((prev) => (prev[id] === height ? prev : { ...prev, [id]: height }))
485487
}, [])
@@ -536,11 +538,6 @@ export function ToastProvider({ children }: { children?: ReactNode }) {
536538
}
537539
}, [])
538540

539-
/** On navigation, clear route-scoped toasts so they don't trail the user; `persistAcrossRoutes` toasts survive. */
540-
useEffect(() => {
541-
dismissRouteScopedToasts()
542-
}, [pathname, dismissRouteScopedToasts])
543-
544541
/** Held in a ref (seeded once from the stable `addToast`) so the module-level `toast` binds to the live provider. */
545542
const toastFn = useRef<ToastFn>(createToastFn(addToast))
546543

0 commit comments

Comments
 (0)