Frontend
medium
mid
How would you queue toasts so they don't all stack at once
Keep two lists: visible (capped at N) and a queue. When a toast is dismissed, promote the next queued one. Track timers per visible toast. Optionally de-dupe and prioritize errors. The key is separating 'added' from 'shown'.
4 min read·~12 min to think through
The fix is separating "a toast was requested" from "a toast is on screen."
The data model
js
const MAX_VISIBLE = 3;
// state:
// visible: Toast[] — currently rendered, max MAX_VISIBLE
// queue: Toast[] — waiting for a free slotWhen add(toast) is called:
- If
visible.length < MAX_VISIBLE→ push intovisible, start its dismiss timer. - Else → push into
queue.
When a toast is dismissed (timer or user):
- Remove it from
visible. - If
queueis non-empty →shift()the next one intovisibleand start its timer.
js
function dismiss(id) {
setVisible((v) => {
const next = v.filter((t) => t.id !== id);
if (queueRef.current.length && next.length < MAX_VISIBLE) {
const promoted = queueRef.current.shift();
startTimer(promoted);
return [...next, promoted];
}
return next;
});
}Refinements that show seniority
- Timers only for visible toasts — a queued toast's duration shouldn't count down while it's invisible. Start the timer on promotion, not on add.
- Priority — errors jump the queue (
unshiftinstead ofpush, or sort by priority) so a critical error isn't stuck behind info toasts. - De-duplication — if the same message is added repeatedly, collapse it (or show a "×3" counter) instead of queueing duplicates.
- Queue cap — even the queue should have a limit; drop or coalesce beyond it so a runaway loop doesn't grow memory unboundedly.
- Group/replace — a "Saving…" toast replaced by "Saved" rather than stacking.
The framing
"The bug is treating 'added' and 'shown' as the same thing. I keep two lists — a visible array capped at N with live timers, and a queue for the rest. On dismiss, I promote the next queued toast and start its timer then — so duration only counts while on screen. From there: errors get priority, identical toasts de-dupe, and the queue itself is capped."
Follow-up questions
- •Why start a queued toast's timer on promotion rather than on add?
- •How would you let error toasts jump the queue?
- •How do you handle 100 identical toasts fired in a loop?
- •Should the queue itself have a maximum size?
Common mistakes
- •Starting the dismiss timer when the toast is added, so queued toasts expire while invisible.
- •No cap on the queue — unbounded memory growth.
- •Treating all toasts as equal priority, burying critical errors.
- •Not de-duplicating, so a render loop spams identical toasts.
Performance considerations
- •The queue can grow unbounded under a runaway loop — cap it and coalesce duplicates. Keep per-toast timers in a ref keyed by id and clear them on dismiss/unmount to avoid leaks.
Edge cases
- •Queue grows faster than toasts are dismissed.
- •A sticky (no-duration) toast occupying a visible slot forever.
- •User manually dismisses a toast — must immediately promote from the queue.
- •Identical messages fired rapidly.
Real-world examples
- •Sonner and react-hot-toast cap visible toasts and stack the rest.
- •Chat apps coalescing rapid notifications into a grouped one.
Senior engineer discussion
Seniors immediately separate 'added' from 'shown', cap the visible set, and crucially start timers on promotion. They then layer priority for errors, de-duplication, and a queue cap so a runaway producer can't exhaust memory.
Related questions
Frontend
Hard
5 min