Skip to content

Commit 8a2d5dd

Browse files
authored
fix: skip animation with async to infinite loop (#1384)
* fix: add stopAsync call in async animation loop * feat: add handle to not even start if skipAnimation is set * feat: handle mid async loop skipAnimation setting
1 parent 0fc832c commit 8a2d5dd

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-1
lines changed

packages/core/src/Controller.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,52 @@ describe('Controller', () => {
256256
// Since we call `update` twice, frames are generated!
257257
expect(global.getFrames(ctrl)).toMatchSnapshot()
258258
})
259+
260+
describe('when skipAnimations is true', () => {
261+
it('should not run at all', async () => {
262+
const ctrl = new Controller({ from: { x: 0 } })
263+
let n = 0
264+
265+
global.setSkipAnimation(true)
266+
267+
ctrl.start({
268+
to: async next => {
269+
while (true) {
270+
n += 1
271+
await next({ x: 1, reset: true })
272+
}
273+
},
274+
})
275+
276+
await flushMicroTasks()
277+
expect(n).toBe(0)
278+
})
279+
280+
it('should stop running and push the animation to the finished state when called mid animation', async () => {
281+
const ctrl = new Controller({ from: { x: 0 } })
282+
let n = 0
283+
284+
ctrl.start({
285+
to: async next => {
286+
while (n < 5) {
287+
n++
288+
await next({ x: 10, reset: true })
289+
}
290+
},
291+
})
292+
293+
await global.advance()
294+
expect(n).toBe(1)
295+
296+
global.setSkipAnimation(true)
297+
298+
await global.advanceUntilIdle()
299+
300+
const { x } = ctrl.springs
301+
expect(n).toBe(2)
302+
expect(x.get()).toEqual(10)
303+
})
304+
})
259305
})
260306

261307
describe('when the "onStart" prop is defined', () => {

packages/core/src/runAsync.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { is, raf, flush, eachProp, Timeout } from '@react-spring/shared'
1+
import {
2+
is,
3+
raf,
4+
flush,
5+
eachProp,
6+
Timeout,
7+
Globals as G,
8+
} from '@react-spring/shared'
29
import { Falsy } from '@react-spring/types'
310

411
import { getDefaultProps } from './helpers'
@@ -88,8 +95,23 @@ export function runAsync<T extends AnimationTarget>(
8895
// Create the bail signal outside the returned promise,
8996
// so the generated stack trace is relevant.
9097
const bailSignal = new BailSignal()
98+
const skipAnimationSignal = new SkipAniamtionSignal()
9199

92100
return (async () => {
101+
if (G.skipAnimation) {
102+
/**
103+
* We need to stop animations if `skipAnimation`
104+
* is set in the Globals
105+
*
106+
*/
107+
stopAsync(state)
108+
109+
// create the rejection error that's handled gracefully
110+
skipAnimationSignal.result = getFinishedResult(target, false)
111+
bail(skipAnimationSignal)
112+
throw skipAnimationSignal
113+
}
114+
93115
bailIfEnded(bailSignal)
94116

95117
const props: any = is.obj(arg1) ? { ...arg1 } : { ...arg2, to: arg1 }
@@ -115,6 +137,16 @@ export function runAsync<T extends AnimationTarget>(
115137
}
116138

117139
let result!: AnimationResult<T>
140+
141+
if (G.skipAnimation) {
142+
/**
143+
* We need to stop animations if `skipAnimation`
144+
* is set in the Globals
145+
*/
146+
stopAsync(state)
147+
return getFinishedResult(target, false)
148+
}
149+
118150
try {
119151
let animating!: Promise<void>
120152

@@ -139,6 +171,8 @@ export function runAsync<T extends AnimationTarget>(
139171
} catch (err) {
140172
if (err instanceof BailSignal) {
141173
result = err.result
174+
} else if (err instanceof SkipAniamtionSignal) {
175+
result = err.result
142176
} else {
143177
throw err
144178
}
@@ -181,3 +215,11 @@ export class BailSignal extends Error {
181215
)
182216
}
183217
}
218+
219+
export class SkipAniamtionSignal extends Error {
220+
result!: AnimationResult
221+
222+
constructor() {
223+
super('SkipAnimationSignal')
224+
}
225+
}

packages/core/test/setup.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ declare global {
3737

3838
// @ts-ignore
3939
setTimeout: (handler: Function, ms: number) => number
40+
41+
setSkipAnimation: (skip: boolean) => void
4042
}
4143
}
4244
}
@@ -60,6 +62,7 @@ beforeEach(() => {
6062
now: global.mockRaf.now,
6163
requestAnimationFrame: global.mockRaf.raf,
6264
colors,
65+
skipAnimation: false,
6366
// This lets our useTransition hook force its component
6467
// to update from within an "onRest" handler.
6568
batchedUpdates: act,
@@ -184,3 +187,9 @@ global.advanceUntilValue = (spring, value) => {
184187
return stop
185188
})
186189
}
190+
191+
global.setSkipAnimation = skip => {
192+
Globals.assign({
193+
skipAnimation: skip,
194+
})
195+
}

0 commit comments

Comments
 (0)