@@ -6,6 +6,7 @@ import { SpringContextProvider, type ISpringContext } from '../SpringContext'
66import { SpringValue } from '../SpringValue'
77import { SpringRef } from '../SpringRef'
88import { useSpring } from './useSpring'
9+ import { useSpringRef } from './useSpringRef'
910
1011describe ( 'useSpring' , ( ) => {
1112 let springs : Lookup < SpringValue >
@@ -108,6 +109,119 @@ describe('useSpring', () => {
108109 testIsRef ( ref )
109110 } )
110111 } )
112+
113+ // Regression test for https://github.com/pmndrs/react-spring/issues/1991
114+ describe ( 'when an external SpringRef is attached via the `ref` prop' , ( ) => {
115+ it ( 'fires events declared on render when `ref.start()` is called with no args' , async ( ) => {
116+ const externalRef = SpringRef ( )
117+ const onStart = vi . fn ( )
118+ const onRest = vi . fn ( )
119+
120+ function Component ( ) {
121+ useSpring ( {
122+ ref : externalRef ,
123+ from : { x : 0 } ,
124+ to : { x : 100 } ,
125+ onStart,
126+ onRest,
127+ } )
128+ return null
129+ }
130+
131+ render ( < Component /> )
132+
133+ // Animation should not start until `ref.start()` is called.
134+ expect ( onStart ) . not . toHaveBeenCalled ( )
135+
136+ externalRef . start ( )
137+ await advanceUntilIdle ( )
138+
139+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 )
140+ expect ( onRest ) . toHaveBeenCalledTimes ( 1 )
141+ } )
142+
143+ it ( 'fires events under StrictMode (double-mount)' , async ( ) => {
144+ const onStart = vi . fn ( )
145+ const onRest = vi . fn ( )
146+ let capturedRef : SpringRef | undefined
147+
148+ function Component ( ) {
149+ const springRef = useSpringRef ( )
150+ capturedRef = springRef
151+
152+ useSpring ( {
153+ ref : springRef ,
154+ from : { x : 0 } ,
155+ to : { x : 100 } ,
156+ onStart,
157+ onRest,
158+ } )
159+
160+ React . useEffect ( ( ) => {
161+ springRef . start ( )
162+ } , [ springRef ] )
163+
164+ return null
165+ }
166+
167+ render (
168+ < React . StrictMode >
169+ < Component />
170+ </ React . StrictMode >
171+ )
172+ await advanceUntilIdle ( )
173+
174+ expect ( capturedRef ) . toBeDefined ( )
175+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 )
176+ expect ( onRest ) . toHaveBeenCalledTimes ( 1 )
177+ } )
178+
179+ // Exact reproduction from the issue body.
180+ it ( 'fires events when `springRef.start()` is called from a useEffect (issue #1991 repro)' , async ( ) => {
181+ const onStart = vi . fn ( )
182+ const onRest = vi . fn ( )
183+ let capturedRef : SpringRef | undefined
184+
185+ function Component ( ) {
186+ const springRef = useSpringRef ( )
187+ capturedRef = springRef
188+
189+ useSpring ( {
190+ ref : springRef ,
191+ config : { duration : 1000 } ,
192+ from : {
193+ position : 'relative' ,
194+ opacity : 1 ,
195+ right : 0 ,
196+ } ,
197+ to : {
198+ position : 'relative' ,
199+ opacity : 0 ,
200+ right : - 300 ,
201+ } ,
202+ onStart,
203+ onRest,
204+ } )
205+
206+ React . useEffect ( ( ) => {
207+ springRef . start ( )
208+ } , [ springRef ] )
209+
210+ return null
211+ }
212+
213+ render (
214+ < React . StrictMode >
215+ < Component />
216+ </ React . StrictMode >
217+ )
218+ await advanceUntilIdle ( )
219+
220+ expect ( capturedRef ) . toBeDefined ( )
221+ expect ( onStart ) . toHaveBeenCalledTimes ( 1 )
222+ expect ( onRest ) . toHaveBeenCalledTimes ( 1 )
223+ } )
224+ } )
111225} )
112226
113227interface TestContext extends ISpringContext {
0 commit comments