Skip to content

Commit 8a06ca7

Browse files
authored
fix(core)!: stop firing onRest when an active animation is retargeted (#2467)
1 parent 7b9a6e6 commit 8a06ca7

3 files changed

Lines changed: 36 additions & 11 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@react-spring/core': major
3+
---
4+
5+
**Breaking:** `onRest` no longer fires when an active animation is retargeted
6+
7+
Calling `spring.start()` with a new `to` value mid-flight no longer
8+
invokes the previous animation's `onRest` handler. `onRest` is
9+
documented as called when the animation comes to a stand-still, and a
10+
retarget is not a stand-still — the spring keeps moving toward the new
11+
goal. The `start()` promise still resolves with `finished: false` on
12+
retarget, so callers that need the old goal-abandoned signal can read
13+
it there. `onRest` continues to fire as before on `reset`, on `cancel`,
14+
and on normal settle.
15+
16+
**Migration:** If you relied on `onRest` running on every retarget
17+
(e.g. for cleanup or analytics), inspect the `start()` promise's
18+
`finished: false` resolution instead, or move the side effect into
19+
`onChange`.

packages/core/src/SpringValue.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,15 @@ function describeToProp() {
202202
expect(onStart).toBeCalledTimes(1)
203203
})
204204

205-
it.todo('avoids calling the "onRest" prop')
205+
it('avoids calling the "onRest" prop', async () => {
206+
const onRest = vi.fn()
207+
const spring = new SpringValue(0)
208+
spring.start(1, { onRest })
209+
await global.advance(5)
210+
spring.start(2)
211+
await global.advanceUntilIdle()
212+
expect(onRest).not.toBeCalled()
213+
})
206214
})
207215

208216
describe('when "to" prop equals current value', () => {

packages/core/src/SpringValue.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -851,18 +851,16 @@ export class SpringValue<T = any> extends FrameValue<T> {
851851
// Ensure `onStart` can be called after a reset.
852852
anim.changed = !reset
853853

854-
// Call the active `onRest` handler from the interrupted animation.
855-
onRest?.(result, this)
856-
857-
// Notify the default `onRest` of the reset, but wait for the
858-
// first frame to pass before sending an `onStart` event.
859854
if (reset) {
855+
// Notify the previous animation's `onRest` that it did not
856+
// finish, then the default `onRest`, before jumping back to
857+
// `from` on the next frame.
858+
onRest?.(result, this)
860859
callProp(defaultProps.onRest, result)
861-
}
862-
// Call the active `onStart` handler here since the first frame
863-
// has already passed, which means this is a goal update and not
864-
// an entirely new animation.
865-
else {
860+
} else {
861+
// Goal update mid-flight: notify the active `onStart` that we
862+
// are animating toward a new target. The spring never came to
863+
// a stand-still, so do not fire `onRest`.
866864
anim.onStart?.(result, this)
867865
}
868866
})

0 commit comments

Comments
 (0)