@@ -48,21 +48,44 @@ const ToasterComponent = forwardRef<ToasterRef, ToasterProps>(
4848 const replacementTimerRef = useRef < ReturnType < typeof setTimeout > | null > (
4949 null ,
5050 ) ;
51- const dismissTimerRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
51+ // Separate timers to avoid cleanup collisions.
52+ const autoDismissTimerRef = useRef < ReturnType < typeof setTimeout > | null > (
53+ null ,
54+ ) ;
55+ const exitCleanupTimerRef = useRef < ReturnType < typeof setTimeout > | null > (
56+ null ,
57+ ) ;
58+ // Track pending rAF callbacks for enter animation.
59+ const enterRafId1Ref = useRef < number | null > ( null ) ;
60+ const enterRafId2Ref = useRef < number | null > ( null ) ;
5261 const innerRef = useRef < ToasterRef | null > ( null ) ;
5362
5463 const closeToast = ( ) => {
64+ // Cancel any pending enter rAFs to prevent stale visibility updates.
65+ if ( enterRafId1Ref . current !== null ) {
66+ cancelAnimationFrame ( enterRafId1Ref . current ) ;
67+ enterRafId1Ref . current = null ;
68+ }
69+ if ( enterRafId2Ref . current !== null ) {
70+ cancelAnimationFrame ( enterRafId2Ref . current ) ;
71+ enterRafId2Ref . current = null ;
72+ }
5573 if ( replacementTimerRef . current !== null ) {
5674 clearTimeout ( replacementTimerRef . current ) ;
5775 replacementTimerRef . current = null ;
5876 }
59- if ( dismissTimerRef . current !== null ) {
60- clearTimeout ( dismissTimerRef . current ) ;
61- dismissTimerRef . current = null ;
77+ // Clear any auto-dismiss timer and any prior exit cleanup timer.
78+ if ( autoDismissTimerRef . current !== null ) {
79+ clearTimeout ( autoDismissTimerRef . current ) ;
80+ autoDismissTimerRef . current = null ;
81+ }
82+ if ( exitCleanupTimerRef . current !== null ) {
83+ clearTimeout ( exitCleanupTimerRef . current ) ;
84+ exitCleanupTimerRef . current = null ;
6285 }
6386 setIsVisible ( false ) ;
64- dismissTimerRef . current = setTimeout ( ( ) => {
65- dismissTimerRef . current = null ;
87+ exitCleanupTimerRef . current = setTimeout ( ( ) => {
88+ exitCleanupTimerRef . current = null ;
6689 setToastOptions ( undefined ) ;
6790 } , TOAST_ANIMATION_DURATION ) ;
6891 } ;
@@ -73,9 +96,14 @@ const ToasterComponent = forwardRef<ToasterRef, ToasterProps>(
7396
7497 if ( toastOptions ) {
7598 setIsVisible ( false ) ;
76- if ( dismissTimerRef . current !== null ) {
77- clearTimeout ( dismissTimerRef . current ) ;
78- dismissTimerRef . current = null ;
99+ // Clear any existing timers when replacing an in-flight toast.
100+ if ( autoDismissTimerRef . current !== null ) {
101+ clearTimeout ( autoDismissTimerRef . current ) ;
102+ autoDismissTimerRef . current = null ;
103+ }
104+ if ( exitCleanupTimerRef . current !== null ) {
105+ clearTimeout ( exitCleanupTimerRef . current ) ;
106+ exitCleanupTimerRef . current = null ;
79107 }
80108 timeoutDuration = TOAST_ANIMATION_DURATION ;
81109 setToastOptions ( undefined ) ;
@@ -107,24 +135,36 @@ const ToasterComponent = forwardRef<ToasterRef, ToasterProps>(
107135 // Trigger enter animation after toast is mounted in the DOM.
108136 useEffect ( ( ) => {
109137 if ( toastOptions && ! isVisible ) {
110- requestAnimationFrame ( ( ) => {
111- requestAnimationFrame ( ( ) => {
138+ enterRafId1Ref . current = requestAnimationFrame ( ( ) => {
139+ enterRafId2Ref . current = requestAnimationFrame ( ( ) => {
112140 setIsVisible ( true ) ;
113141 } ) ;
114142 } ) ;
143+ // Cleanup to cancel pending rAFs if toast is dismissed quickly.
144+ return ( ) => {
145+ if ( enterRafId1Ref . current !== null ) {
146+ cancelAnimationFrame ( enterRafId1Ref . current ) ;
147+ enterRafId1Ref . current = null ;
148+ }
149+ if ( enterRafId2Ref . current !== null ) {
150+ cancelAnimationFrame ( enterRafId2Ref . current ) ;
151+ enterRafId2Ref . current = null ;
152+ }
153+ } ;
115154 }
155+ return undefined ;
116156 } , [ toastOptions ] ) ; // intentionally omit isVisible — only react to new toast options
117157
118158 // Auto-dismiss timer.
119159 useEffect ( ( ) => {
120160 if ( isVisible && toastOptions && ! toastOptions . hasNoTimeout ) {
121- dismissTimerRef . current = setTimeout ( ( ) => {
122- dismissTimerRef . current = null ;
161+ autoDismissTimerRef . current = setTimeout ( ( ) => {
162+ autoDismissTimerRef . current = null ;
123163 innerRef . current ?. closeToast ( ) ;
124164 } , TOAST_VISIBILITY_DURATION ) ;
125165 return ( ) => {
126- if ( dismissTimerRef . current !== null ) {
127- clearTimeout ( dismissTimerRef . current ) ;
166+ if ( autoDismissTimerRef . current !== null ) {
167+ clearTimeout ( autoDismissTimerRef . current ) ;
128168 }
129169 } ;
130170 }
0 commit comments