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

Walkthrough

Space play step click any line
Three takeaways
  1. 1A keyed Map lets you collapse duplicate events into a single entry with a running count instead of piling up noise.
  2. 2Pairing each entry with its own timer and cancelling before rescheduling keeps auto-dismissal correct as state changes.
  3. 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.

A self-dismissing toast store with dedup — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code