It seems this package has a memory leak where dismissed toasts are never removed from the global state, causing the toasts array to grow indefinitely throughout the application's lifetime.
The Observer class in state.ts maintains a this.toasts array that accumulates every toast ever created, but never removes them when they are dismissed or closed.
The problem becomes significantly more severe when using custom JSX content.
Each toast object retains full React element trees with component instances, hooks, event handlers, and DOM references in this.toasts.
These never get garbage collected since the global array holds strong references, leading to exponentially higher memory usage compared to primitive title / description strings.
Simple strings use ~1-5KB per toast; complex JSX can exceed 10-50KB each, causing gigabytes of leaks in long-running SPAs.
Example: Crypto Exchange.
Demo
addToast only appends more items
|
addToast = (data: ToastT) => { |
|
this.publish(data); |
|
this.toasts = [...this.toasts, data]; |
|
}; |
dismiss doesn't clean up the this.toasts array
|
dismiss = (id?: number | string) => { |
|
if (id) { |
|
this.dismissedToasts.add(id); |
|
requestAnimationFrame(() => this.subscribers.forEach((subscriber) => subscriber({ id, dismiss: true }))); |
|
} else { |
|
this.toasts.forEach((toast) => { |
|
this.subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true })); |
|
}); |
|
} |
|
|
|
return id; |
|
}; |
-
The dismissed toasts remain in this.toasts forever, only tracked separately in this.dismissedToasts Set.
-
The component state in index.tsx properly filters toasts
|
setActiveToasts((toasts) => toasts.filter((t) => t.id !== toast.id)); |
|
return toasts.filter(({ id }) => id !== toastToRemove.id); |
However, the global ToastState.toasts in the Observer class is never cleaned up.
Proposed Solutions
- Adding a maximum history size with LRU eviction
OR
- Adding a
clearHistory() method
OR
- Adding cleanup logic to the
dismiss method (this should be a little bit harder due to the animations)
It seems this package has a memory leak where dismissed toasts are never removed from the global state, causing the
toastsarray to grow indefinitely throughout the application's lifetime.The
Observerclass in state.ts maintains athis.toastsarray that accumulates every toast ever created, but never removes them when they are dismissed or closed.The problem becomes significantly more severe when using custom JSX content.
Each toast object retains full React element trees with component instances, hooks, event handlers, and DOM references in
this.toasts.These never get garbage collected since the global array holds strong references, leading to exponentially higher memory usage compared to primitive
title/descriptionstrings.Simple strings use ~1-5KB per toast; complex JSX can exceed 10-50KB each, causing gigabytes of leaks in long-running SPAs.
Example: Crypto Exchange.
Demo
addToastonly appends more itemssonner/src/state.ts
Lines 42 to 45 in 45d8940
dismissdoesn't clean up thethis.toastsarraysonner/src/state.ts
Lines 88 to 99 in 45d8940
The dismissed toasts remain in
this.toastsforever, only tracked separately inthis.dismissedToastsSet.The component state in index.tsx properly filters toasts
sonner/src/index.tsx
Line 560 in 45d8940
sonner/src/index.tsx
Line 652 in 45d8940
However, the global
ToastState.toastsin the Observer class is never cleaned up.Proposed Solutions
OR
clearHistory()methodOR
dismissmethod (this should be a little bit harder due to the animations)