Skip to content

[bug]: Incorrect result type in SpringValue onChange handler #2183

Closed

Description

@mattrossman

Which react-spring target are you using?

  • @react-spring/web
  • @react-spring/three
  • @react-spring/native
  • @react-spring/konva
  • @react-spring/zdog

What version of react-spring are you using?

9.7.3

What's Wrong?

The onChange option in the SpringValue constructor indicates that the argument is of type AnimationResult<SpringValue<number>>

CleanShot 2023-08-03 at 13 48 53@2x

Thus, result.value is indicated to be of type number, and logging this should display the animated value. However, it actually prints undefined as the spring animates:

CleanShot 2023-08-03 at 13 49 39@2x

If I instead log the entire result, I see the number.

To Reproduce

import { SpringValue } from '@react-spring/web'

const spring = new SpringValue(0, {
 onChange(result) {
  console.log(result.value)
 }
})

spring.start(1)

Expected Behaviour

I expect to see the value on result.value, which TypeScript indicates is a number

Link to repo

https://codesandbox.io/s/vigorous-mirzakhani-qrrwj8?file=/src/App.tsx

Activity

  1. Bilalshaikh15 commented on Aug 9, 2023

    @Bilalshaikh15

    The onChange callback of the SpringValue is triggered whenever the animation state updates, which includes both the start and end states of the animation. However, at the start of the animation, the value property of the animation result might not have been set yet, leading to undefined being logged.

    If you want to log the animated value specifically when it changes, you should use the onRest callback instead. The onRest callback is triggered when the animation completes and the final value is available. Here's an example of how you might modify your code:

    const [spring] = useState(
        () =>
          new SpringValue(0, {
            onRest(result) {
              console.log(result.value);
            }
          })

    so if you log result.value now the number 1 or 0 would be printed in the console.

    Please let me know did i fix th issue,
    as i am new in contributing to open source can you tell me the project file location so i could commit changes.

  2. mattrossman commented on Aug 9, 2023

    @mattrossman
    Author

    Thank you for the suggestion. You're correct that within onRest, result contains and AnimationResult as expected. I'm specifically trying to use onChange here though, as I want to read intermediate values throughout the transition.

    The reason my example is printing undefined is because result contains the actual value (a number) in the onChange callback instead of an AnimationResult type, and trying to read the "value" property from a number is undefined.

    I can get the output I want by logging the result itself: https://codesandbox.io/s/busy-hugle-5w6px4?file=/src/App.tsx

    The change I'm requesting is to ensure result holds an AnimationResult instead of a plain value to be consistent with the type signature and other callbacks.

  3. incon commented on Apr 3, 2025

    @incon

    I'm having the same Typescript type issues. The onChange value is a number type and not the AnimationResult<SpringValue> type.

  4. sadan4 commented on Oct 21, 2025

    @sadan4

    Just ran into this after wondering why nothing was working for a while. Would be nice to get this fixed.

  5. added this to the milestone on May 23, 2026
  6. removed this from the milestone on Jun 11, 2026
  7. joshuaellis commented on Jun 19, 2026

    @joshuaellis
    Member

    Not an any leak — the type is already correct (result.value is number). It's a runtime/type mismatch: the SpringValue-level onChange is called with the raw value instead of an AnimationResult, so result.value is undefined.

    SpringValue._onChange (packages/core/src/SpringValue.ts):

    protected _onChange(value: T, idle?: boolean) {
      if (!idle) {
        this._onStart()
        callProp(this.animation.onChange, value, this)   // raw value
      }
      callProp(this.defaultProps.onChange, value, this)  // raw value
      super._onChange(value, idle)
    }

    onChange(result) is handed 5, so result.value is (5).value → undefined. Logging the whole result shows 5 because result is the value itself.

    onChange is the only event that does this. onStart and onRest already dispatch an AnimationResult (getFinishedResult/getCancelledResult), and the Controller-level onChange sets result.value before calling the handler. Only the SpringValue-level onChange skips it.

    Fix

    Wrap the value in an AnimationResult, mirroring onStart/onRest:

    protected _onChange(value: T, idle?: boolean) {
      const result = getFinishedResult(value, false) // { value, finished: false, cancelled: false }
      if (!idle) {
        this._onStart()
        callProp(this.animation.onChange, result, this)
      }
      callProp(this.defaultProps.onChange, result, this)
      super._onChange(value, idle) // leave raw — internal fluid observers depend on it
    }

    Leave super._onChange(value) untouched: that emits the internal change event the Controller/animated tree subscribe to, which expects the raw value.

    This aligns the runtime with the AnimationResult contract the type already advertises, so typed code keeps compiling — result.value just stops being undefined. Pair it with a runtime test asserting result.value matches the spring's current value during a change; the type side is covered by a .test-d guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: corerelates to core classes // parts of the librarykind: bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions