-
-
Notifications
You must be signed in to change notification settings - Fork 417
Description
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.
- Relates to Toast DOM nodes are leaked #605 cc @Ducklett
Demo
addToastonly appends more items
Lines 42 to 45 in 45d8940
| addToast = (data: ToastT) => { | |
| this.publish(data); | |
| this.toasts = [...this.toasts, data]; | |
| }; |
dismissdoesn't clean up thethis.toastsarray
Lines 88 to 99 in 45d8940
| 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.toastsforever, only tracked separately inthis.dismissedToastsSet. -
The component state in index.tsx properly filters toasts
Line 560 in 45d8940
| setActiveToasts((toasts) => toasts.filter((t) => t.id !== toast.id)); |
Line 652 in 45d8940
| 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
dismissmethod (this should be a little bit harder due to the animations)