@@ -7,12 +7,15 @@ export interface ToastOptions {
77 description ?: string ;
88 duration ?: number ;
99 variant ?: ToastVariant ;
10+ /** Unique key for deduplication. If not provided, deduplication uses title+description */
11+ dedupeKey ?: string ;
1012}
1113
1214interface ToastItem extends ToastOptions {
1315 id : string ;
1416 variant : ToastVariant ;
1517 duration : number ;
18+ timestamp : number ;
1619}
1720
1821interface ToastContextType {
@@ -22,18 +25,33 @@ interface ToastContextType {
2225 error : ( options : Omit < ToastOptions , "variant" > ) => string ;
2326 warning : ( options : Omit < ToastOptions , "variant" > ) => string ;
2427 info : ( options : Omit < ToastOptions , "variant" > ) => string ;
28+ clearAll : ( ) => void ;
2529}
2630
2731const ToastContext = createContext < ToastContextType | undefined > ( undefined ) ;
2832
2933const DEFAULT_DURATION = 5000 ;
34+ const DEDUPE_WINDOW_MS = 3000 ; // Don't show duplicate toasts within 3 seconds
35+
36+ /**
37+ * Generate a deduplication key from toast content.
38+ * Toasts with the same key within DEDUPE_WINDOW_MS are considered duplicates.
39+ */
40+ function generateDedupeKey ( options : ToastOptions ) : string {
41+ if ( options . dedupeKey ) {
42+ return options . dedupeKey ;
43+ }
44+ return `${ options . title } |${ options . description || '' } |${ options . variant || 'info' } ` ;
45+ }
3046
3147export const ToastProvider : React . FC < { children : React . ReactNode } > = ( {
3248 children,
3349} ) => {
3450 const [ toasts , setToasts ] = useState < ToastItem [ ] > ( [ ] ) ;
3551 const nextToastId = useRef ( 0 ) ;
3652 const timeoutRefs = useRef < Map < string , number > > ( new Map ( ) ) ;
53+ // Track recent toast keys for deduplication
54+ const recentToasts = useRef < Map < string , number > > ( new Map ( ) ) ;
3755
3856 const dismissToast = ( id : string ) => {
3957 setToasts ( ( currentToasts ) => currentToasts . filter ( ( toast ) => toast . id !== id ) ) ;
@@ -45,17 +63,40 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
4563 }
4664 } ;
4765
66+ const clearAll = ( ) => {
67+ setToasts ( [ ] ) ;
68+ timeoutRefs . current . forEach ( ( timeoutId ) => {
69+ window . clearTimeout ( timeoutId ) ;
70+ } ) ;
71+ timeoutRefs . current . clear ( ) ;
72+ } ;
73+
4874 const showToast = ( {
4975 variant = "info" ,
5076 duration = DEFAULT_DURATION ,
5177 ...options
5278 } : ToastOptions ) => {
79+ const dedupeKey = generateDedupeKey ( { ...options , variant } ) ;
80+ const now = Date . now ( ) ;
81+
82+ // Check for duplicate within dedupe window
83+ const lastShown = recentToasts . current . get ( dedupeKey ) ;
84+ if ( lastShown && now - lastShown < DEDUPE_WINDOW_MS ) {
85+ // Duplicate detected - return the existing toast ID (we don't have it, so return empty)
86+ // In a production system, you might want to bump the existing toast or extend its duration
87+ return '' ;
88+ }
89+
90+ // Record this toast for deduplication
91+ recentToasts . current . set ( dedupeKey , now ) ;
92+
5393 nextToastId . current += 1 ;
5494 const id = `toast-${ nextToastId . current } ` ;
5595 const nextToast : ToastItem = {
5696 id,
5797 variant,
5898 duration,
99+ timestamp : now ,
59100 ...options ,
60101 } ;
61102
@@ -69,6 +110,22 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
69110 return id ;
70111 } ;
71112
113+ // Clean up old dedupe entries periodically
114+ useEffect ( ( ) => {
115+ const cleanupInterval = setInterval ( ( ) => {
116+ const now = Date . now ( ) ;
117+ const staleKeys : string [ ] = [ ] ;
118+ recentToasts . current . forEach ( ( timestamp , key ) => {
119+ if ( now - timestamp > DEDUPE_WINDOW_MS ) {
120+ staleKeys . push ( key ) ;
121+ }
122+ } ) ;
123+ staleKeys . forEach ( ( key ) => recentToasts . current . delete ( key ) ) ;
124+ } , DEDUPE_WINDOW_MS ) ;
125+
126+ return ( ) => clearInterval ( cleanupInterval ) ;
127+ } , [ ] ) ;
128+
72129 useEffect ( ( ) => {
73130 const timeouts = timeoutRefs . current ;
74131
@@ -85,6 +142,7 @@ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
85142 value = { {
86143 showToast,
87144 dismissToast,
145+ clearAll,
88146 success : ( options ) => showToast ( { ...options , variant : "success" } ) ,
89147 error : ( options ) => showToast ( { ...options , variant : "error" } ) ,
90148 warning : ( options ) => showToast ( { ...options , variant : "warning" } ) ,
0 commit comments