typescript
72 lines · 10 steps
A self-dismissing toast store with dedup
A subscribable toast stack that deduplicates repeats, auto-expires entries, and notifies listeners on every change.
Explained by
highlit
1type ToastVariant = "info" | "success" | "warning" | "error";
2
3interface Toast {
4 id: string;
5 message: string;
6 variant: ToastVariant;
7 count: number;
8 createdAt: number;
9 updatedAt: number;
10}
11
12type Listener = (toasts: Toast[]) => void;
13
14export class ToastStack {
15 private toasts = new Map<string, Toast>();
16 private timers = new Map<string, ReturnType<typeof setTimeout>>();
17 private listeners = new Set<Listener>();
18
19 constructor(private readonly ttl = 4000) {}
20
21 subscribe(fn: Listener): () => void {
22 this.listeners.add(fn);
23 fn(this.snapshot());
24 return () => this.listeners.delete(fn);
25 }
26
27 push(message: string, variant: ToastVariant = "info"): string {
28 const key = `${variant}:${message}`;
29 const now = Date.now();
30 const existing = this.toasts.get(key);
31
32 if (existing) {
33 existing.count += 1;
34 existing.updatedAt = now;
35 } else {
36 this.toasts.set(key, {
37 id: key,
38 message,
39 variant,
40 count: 1,
41 createdAt: now,
42 updatedAt: now,
43 });
44 }
45
46 this.scheduleDismiss(key);
47 this.emit();
48 return key;
49 }
50
51 dismiss(id: string): void {
52 if (this.toasts.delete(id)) {
53 clearTimeout(this.timers.get(id));
54 this.timers.delete(id);
55 this.emit();
56 }
57 }
58
59 private scheduleDismiss(key: string): void {
60 clearTimeout(this.timers.get(key));
61 this.timers.set(key, setTimeout(() => this.dismiss(key), this.ttl));
62 }
63
64 private snapshot(): Toast[] {
65 return [...this.toasts.values()].sort((a, b) => b.updatedAt - a.updatedAt);
66 }
67
68 private emit(): void {
69 const snapshot = this.snapshot();
70 for (const fn of this.listeners) fn(snapshot);
71 }
72}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A keyed Map lets you collapse duplicate events into a single entry with a running count instead of piling up noise.
- 2Pairing each entry with its own timer and cancelling before rescheduling keeps auto-dismissal correct as state changes.
- 3The subscribe/emit pattern decouples state from UI by pushing immutable snapshots to every registered listener.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-self-dismissing-toast-store-with-dedup-explained-typescript-21e4/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.