Skip to content

Sync JS-side AnimatedValue before invoking native-driver completion callback (#57021)#57021

Closed
fabriziocucci wants to merge 1 commit into
facebook:mainfrom
fabriziocucci:export-D106940382
Closed

Sync JS-side AnimatedValue before invoking native-driver completion callback (#57021)#57021
fabriziocucci wants to merge 1 commit into
facebook:mainfrom
fabriziocucci:export-D106940382

Conversation

@fabriziocucci
Copy link
Copy Markdown
Contributor

@fabriziocucci fabriziocucci commented Jun 1, 2026

Summary:

Animation.__startAnimationIfNative invoked the user's start({finished})
callback BEFORE syncing the JS-side AnimatedValue with the post-animation
value reported by the native side. Any caller that read the AnimatedValue
from inside the callback, or from a React re-render that the callback
triggered, observed the pre-animation value, producing visual jumps.

Concrete impact: an <Animated.View> whose transform: [{scaleX}] is
driven by value.interpolate({inputRange: [0, 1], outputRange: [0.5, 1]})
would render at scaleX = interp(0) = 0.5 after the animation finished,
instead of at scaleX = interp(1) = 1. The same pattern affects opacity,
color and any other style derived from an animated value read during a
re-render scheduled by the completion callback.

This change reorders the native completion callback so
animatedValue.__onAnimatedValueUpdateReceived(value, offset) runs BEFORE
this.__notifyAnimationEnd(result). The user's callback (and any re-render
it schedules) now observes the post-animation JS value.

The reorder is gated behind a new JS-only feature flag,
animatedShouldSyncValueBeforeStartCallback, which defaults to true
(the fix is on by default). Set the flag to false to opt out and
restore the pre-fix ordering as a kill-switch.

A Fantom integration test in Animated-itest.js exercises the exact
scenario: starts a useNativeDriver: true Animated.timing(0 -> 1),
captures both _value._value and value.interpolate(...).__getValue()
inside the start({finished}) callback and asserts the value matches the
flag state (pre-animation when off, post-animation when on).

Behavior change to consider

The reorder also changes the order in which JS-side observers of the
AnimatedValue are notified relative to the start({finished}) callback.
This was confirmed empirically against the current and the proposed
ordering.

Before (flag off):

  1. start({finished}) callback fires
  2. AnimatedValue.addListener(...) subscribers receive the post-animation value

After (flag on):

  1. AnimatedValue.addListener(...) subscribers receive the post-animation value
  2. start({finished}) callback fires

For the vast majority of callers this is irrelevant or strictly better
(observers and callback now agree on the same value). The flag defaults
to true so the fix ships immediately; the flag itself stays as a
kill-switch in case real-world callers turn out to depend on the
previous ordering. Once adoption has been verified the flag can be
removed entirely.

Changelog:
[General][Fixed] - Sync JS-side Animated.Value with the post-animation value before invoking Animated.timing(...).start({finished}) callbacks so reads from inside the callback (or from React re-renders it triggers) observe the post-animation value rather than the pre-animation value. Gated behind a new JS-only feature flag, animatedShouldSyncValueBeforeStartCallback, defaulting to true (set to false to opt out).

Reviewed By: javache, zeyap

Differential Revision: D106940382

@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented Jun 1, 2026

@fabriziocucci has exported this pull request. If you are a Meta employee, you can view the originating Diff in D106940382.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 1, 2026
@facebook-github-tools facebook-github-tools Bot added p: Facebook Partner: Facebook Partner labels Jun 1, 2026
…allback (facebook#57021)

Summary:

`Animation.__startAnimationIfNative` invoked the user's `start({finished})`
callback BEFORE syncing the JS-side `AnimatedValue` with the post-animation
value reported by the native side. Any caller that read the `AnimatedValue`
from inside the callback, or from a React re-render that the callback
triggered, observed the **pre-animation** value, producing visual jumps.

Concrete impact: an `<Animated.View>` whose `transform: [{scaleX}]` is
driven by `value.interpolate({inputRange: [0, 1], outputRange: [0.5, 1]})`
would render at `scaleX = interp(0) = 0.5` after the animation finished,
instead of at `scaleX = interp(1) = 1`. The same pattern affects opacity,
color and any other style derived from an animated value read during a
re-render scheduled by the completion callback.

This change reorders the native completion callback so
`animatedValue.__onAnimatedValueUpdateReceived(value, offset)` runs BEFORE
`this.__notifyAnimationEnd(result)`. The user's callback (and any re-render
it schedules) now observes the post-animation JS value.

The reorder is gated behind a new JS-only feature flag,
`animatedShouldSyncValueBeforeStartCallback`, which defaults to `true`
(the fix is on by default). Set the flag to `false` to opt out and
restore the pre-fix ordering as a kill-switch.

A Fantom integration test in `Animated-itest.js` exercises the exact
scenario: starts a `useNativeDriver: true` `Animated.timing(0 -> 1)`,
captures both `_value._value` and `value.interpolate(...).__getValue()`
inside the `start({finished})` callback and asserts the value matches the
flag state (pre-animation when off, post-animation when on).

## Behavior change to consider

The reorder also changes the order in which JS-side observers of the
`AnimatedValue` are notified relative to the `start({finished})` callback.
This was confirmed empirically against the current and the proposed
ordering.

Before (flag off):
1. `start({finished})` callback fires
2. `AnimatedValue.addListener(...)` subscribers receive the post-animation value

After (flag on):
1. `AnimatedValue.addListener(...)` subscribers receive the post-animation value
2. `start({finished})` callback fires

For the vast majority of callers this is irrelevant or strictly better
(observers and callback now agree on the same value). The flag defaults
to `true` so the fix ships immediately; the flag itself stays as a
kill-switch in case real-world callers turn out to depend on the
previous ordering. Once adoption has been verified the flag can be
removed entirely.

Changelog:
[General][Fixed] - Sync JS-side `Animated.Value` with the post-animation value before invoking `Animated.timing(...).start({finished})` callbacks so reads from inside the callback (or from React re-renders it triggers) observe the post-animation value rather than the pre-animation value. Gated behind a new JS-only feature flag, `animatedShouldSyncValueBeforeStartCallback`, defaulting to `true` (set to `false` to opt out).

Reviewed By: javache, zeyap

Differential Revision: D106940382
@meta-codesync meta-codesync Bot changed the title Sync JS-side AnimatedValue before invoking native-driver completion callback Sync JS-side AnimatedValue before invoking native-driver completion callback (#57021) Jun 1, 2026
@meta-codesync meta-codesync Bot closed this in ee6958a Jun 1, 2026
@facebook-github-tools facebook-github-tools Bot added the Merged This PR has been merged. label Jun 1, 2026
@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented Jun 1, 2026

This pull request has been merged in ee6958a.

@react-native-bot
Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @fabriziocucci in ee6958a

When will my fix make it into a release? | How to file a pick request?

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

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. fb-exported Merged This PR has been merged. meta-exported p: Facebook Partner: Facebook Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants