diff --git a/.changeset/onrest-cancelled-before-first-frame.md b/.changeset/onrest-cancelled-before-first-frame.md new file mode 100644 index 0000000000..f9406207aa --- /dev/null +++ b/.changeset/onrest-cancelled-before-first-frame.md @@ -0,0 +1,5 @@ +--- +'@react-spring/core': patch +--- + +Fire `onRest` when an animation is cancelled before its first frame. `SpringValue._stop` gated `onRest` on `anim.changed`, which only becomes `true` once a frame has rendered. When two `start()` calls happened within a single tick and the second returned the spring to its current value, the engine stopped the animation before any frame fired, so `onRest` was silently dropped. The same gap affected `set()` and `stop()` called synchronously after `start()`. Closes #1802. diff --git a/packages/core/src/SpringValue.test.ts b/packages/core/src/SpringValue.test.ts index 7309a29b93..46f0326cb6 100644 --- a/packages/core/src/SpringValue.test.ts +++ b/packages/core/src/SpringValue.test.ts @@ -956,6 +956,21 @@ function describeEvents() { global.mockRaf.step() expect(onRest).toBeCalledTimes(1) }) + // https://github.com/pmndrs/react-spring/issues/1802 + it('is called when two starts cancel each other before the first frame', async () => { + const onRest = vi.fn() + const spring = new SpringValue({ from: 0, onRest }) + + // Both starts happen synchronously, before any rAF tick. + // The second start returns to the current value, so the engine + // stops the animation before any frame has rendered. + spring.start(1) + spring.start(0) + + await global.advanceUntilIdle() + + expect(onRest).toBeCalled() + }) }) } diff --git a/packages/core/src/SpringValue.ts b/packages/core/src/SpringValue.ts index caa7a985bb..9cc335cb2b 100644 --- a/packages/core/src/SpringValue.ts +++ b/packages/core/src/SpringValue.ts @@ -1026,10 +1026,8 @@ export class SpringValue extends FrameValue { : getFinishedResult(this.get(), checkFinished(this, goal ?? anim.to)) flushCalls(this._pendingCalls, result) - if (anim.changed) { - anim.changed = false - sendEvent(this, 'onRest', result, this) - } + anim.changed = false + sendEvent(this, 'onRest', result, this) } } }