@@ -3,6 +3,8 @@ import { render } from 'vitest-browser-react'
33import { toArray } from '@react-spring/shared'
44import { TransitionFn , UseTransitionProps } from '../types'
55import { useTransition } from './useTransition'
6+ import { useSpring } from './useSpring'
7+ import { useChain } from './useChain'
68import { SpringRef } from '../SpringRef'
79
810describe ( 'useTransition' , ( ) => {
@@ -153,6 +155,142 @@ describe('useTransition', () => {
153155 testIsRef ( transRef )
154156 } )
155157
158+ // See https://github.com/pmndrs/react-spring/issues/2287
159+ it ( 'auto-starts the enter animation when props is a function with deps' , async ( ) => {
160+ let enterSpring : any = null
161+ const update = createUpdater ( ( { args } ) => {
162+ const transition = toArray ( useTransition ( ...args ) ) [ 0 ]
163+ transition ( style => {
164+ enterSpring = style . n
165+ return null
166+ } )
167+ return null
168+ } )
169+
170+ await update (
171+ true ,
172+ ( ) => ( {
173+ from : { n : 0 } ,
174+ enter : { n : 1 } ,
175+ leave : { n : 0 } ,
176+ } ) ,
177+ [ ]
178+ )
179+
180+ await global . advanceUntilIdle ( )
181+
182+ expect ( enterSpring . get ( ) ) . toEqual ( 1 )
183+ } )
184+
185+ // Guard for the #2287 fix: only the *internal* ref should auto-start. An
186+ // injected ref must keep deferring so manual/imperative control still works.
187+ // This is the inverse of the test above and locks issue #1944.
188+ it ( 'defers the enter animation when an injected ref is provided' , async ( ) => {
189+ const ref = SpringRef ( )
190+ let enterSpring : any = null
191+ const update = createUpdater ( ( { args } ) => {
192+ const transition = toArray ( useTransition ( ...args ) ) [ 0 ]
193+ transition ( style => {
194+ enterSpring = style . n
195+ return null
196+ } )
197+ return null
198+ } )
199+
200+ await update ( true , {
201+ ref,
202+ from : { n : 0 } ,
203+ enter : { n : 1 } ,
204+ leave : { n : 0 } ,
205+ } )
206+
207+ // No `ref.start()` yet — the injected ref defers the enter animation.
208+ await global . advanceUntilIdle ( )
209+ expect ( enterSpring . get ( ) ) . toEqual ( 0 )
210+
211+ // ...and the ref still drives it imperatively.
212+ ref . start ( )
213+ await global . advanceUntilIdle ( )
214+ expect ( enterSpring . get ( ) ) . toEqual ( 1 )
215+ } )
216+
217+ // Guard for the #2287 fix: `useChain` works by draining each controller's
218+ // queue and replaying it in order, which only happens when injected refs
219+ // defer (populate the queue). There was previously zero coverage for useChain.
220+ it ( 'sequences a transition behind a spring with useChain' , async ( ) => {
221+ const springRef = SpringRef ( )
222+ const transRef = SpringRef ( )
223+ let enterSpring : any = null
224+
225+ function Component ( ) {
226+ useSpring ( { ref : springRef , from : { x : 0 } , to : { x : 1 } } )
227+ const transition = toArray (
228+ useTransition ( true , {
229+ ref : transRef ,
230+ from : { n : 0 } ,
231+ enter : { n : 1 } ,
232+ leave : { n : 0 } ,
233+ } )
234+ ) [ 0 ]
235+ transition ( style => {
236+ enterSpring = style . n
237+ return null
238+ } )
239+ useChain ( [ springRef , transRef ] )
240+ return null
241+ }
242+
243+ await render ( < Component /> )
244+
245+ // While the spring is mid-flight, the chained transition stays at `from`.
246+ global . mockRaf . step ( )
247+ expect ( enterSpring . get ( ) ) . toEqual ( 0 )
248+
249+ // Once the chain reaches it, the transition animates to `enter`.
250+ await global . advanceUntilIdle ( )
251+ expect ( enterSpring . get ( ) ) . toEqual ( 1 )
252+ } )
253+
254+ // Guard for the #2287 fix: injected-ref deferral must survive React
255+ // StrictMode's mount → unmount → remount cycle. This is the reattachment
256+ // behaviour `useTransition` adds on mount (#1890/#1944).
257+ it ( 'keeps an injected ref deferred across a StrictMode double-mount' , async ( ) => {
258+ const ref = SpringRef ( )
259+ let enterSpring : any = null
260+
261+ function Component ( ) {
262+ const transition = toArray (
263+ useTransition ( true , {
264+ ref,
265+ from : { n : 0 } ,
266+ enter : { n : 1 } ,
267+ leave : { n : 0 } ,
268+ } )
269+ ) [ 0 ]
270+ transition ( style => {
271+ enterSpring = style . n
272+ return null
273+ } )
274+ return null
275+ }
276+
277+ await render (
278+ < React . StrictMode >
279+ < Component />
280+ </ React . StrictMode >
281+ )
282+
283+ // Deferred even after the simulated unmount/remount.
284+ await global . advanceUntilIdle ( )
285+ expect ( enterSpring . get ( ) ) . toEqual ( 0 )
286+
287+ // Controllers are still attached to the ref, which still drives them.
288+ expect ( ref . current ) . toHaveLength ( 1 )
289+ ref . start ( )
290+ await global . advanceUntilIdle ( )
291+ expect ( enterSpring . get ( ) ) . toEqual ( 1 )
292+ } )
293+
156294 it ( 'passes immediate through to payload' , async ( ) => {
157295 const props = {
158296 from : { n : 0 } ,
0 commit comments