Fix Animated listeners on nodes derived from Animated.Value - #58037
Fix Animated listeners on nodes derived from Animated.Value#58037dennytosp wants to merge 1 commit into
Conversation
`addListener` stopped firing for natively driven animations on nodes derived from an `Animated.Value` — the `Animated.add`/`subtract`/ `multiply`/`divide`/`modulo`/`diffClamp` operators and `.interpolate()`. This regressed in 0.78 with 38c46fe, which moved the native value-update subscription from `AnimatedNode` down into `AnimatedValue` because `startListeningToAnimatedNodeValue` only accepts "value" node tags. The operator and interpolation nodes are value nodes natively (they all derive from `ValueAnimatedNode` / `RCTValueAnimatedNode`), so they lost a subscription they were entitled to. Restore the subscription on `AnimatedNode`, gated on a new `__isNativeValueNode` flag that only nodes backed by a native value node set, preserving the "never listen to a non-value tag" guarantee. Drop the subscription in `__detach` so it cannot outlive its native tag. The C++ backend also rejected these nodes: it compared the exact `AnimatedNodeType::Value` tag rather than testing for a `ValueAnimatedNode` subclass. Replace that with an exhaustive `isValueNodeType` predicate. Fixes react#49719
|
Hi @dennytosp! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Warning JavaScript API change detected This PR commits an update to
This change was flagged as: |
|
Note on the The only removed declarations are two members of declare class AnimatedValue_default extends AnimatedWithChildren_default {
...
- removeAllListeners(): void
- removeListener(id: string): voidBoth are still declared on the base class in the same file, and declare class AnimatedNode_default {
addListener(callback: (value: any) => unknown): string
constructor(config?: null | Readonly<AnimatedNodeConfig> | undefined)
hasListeners(): boolean
removeAllListeners(): void
removeListener(id: string): void
toJSON(): unknown
}
declare class AnimatedWithChildren_default extends AnimatedNode_default {}
The rest of the diff in that file is content-hash churn on symbols that transitively reference |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
Summary:
Fixes #49719.
addListenerstopped firing on anyAnimatedNodederived from anAnimated.Value—Animated.add/subtract/multiply/divide/modulo/diffClampand.interpolate()— for natively driven animations. It worked in 0.77 and regressed in 0.78. The issue has since been reported for bothAnimated.addand.interpolate(), on both architectures, and the only workaround is to listen to the source value and recompute the derived value in JS — which observes intermediate values that never reach the screen.There are two independent causes.
1. JS: derived nodes never subscribe to native updates
Before 38c46fe ("Animated: Lower
onAnimatedValueUpdatetoAnimatedValue", #48514),AnimatedNode.addListenercalledstartListeningToAnimatedNodeValuefor any node that had been made native. That commit moved the logic down intoAnimatedValue, on the premise that:That holds for props/style/transform/object/tracking/color nodes, but not for the operator and interpolation nodes, which are value nodes natively:
AdditionAnimatedNode,SubtractionAnimatedNode,MultiplicationAnimatedNode,DivisionAnimatedNode,ModulusAnimatedNode,DiffClampAnimatedNodeandInterpolationAnimatedNodeall extendValueAnimatedNode, andNativeAnimatedNodesManagerchecksnode !is ValueAnimatedNode.RCT*AnimatedNodeclasses all inheritRCTValueAnimatedNode, and the manager checksisKindOfClass:[RCTValueAnimatedNode class].So the subscription was dropped for a set of nodes that natively support it. This restores it on
AnimatedNode, gated on a new__isNativeValueNodeflag that istrueonly for nodes backed by a native value node. That keeps the guarantee #48514 was after — never callstartListeningToAnimatedNodeValuewith a non-value tag — but states it explicitly instead of leaving it implicit in the class hierarchy.AnimatedValuenow inherits that machinery rather than duplicating it.AnimatedNode.__detachalso drops the subscription beforedropAnimatedNode, so it can never outlive the tag it observes.2. C++:
startListeningToAnimatedNodeValuerejects derived value nodesUnlike Android and iOS, the C++ backend used by the New Architecture compares the node's exact type tag instead of testing for a
ValueAnimatedNodesubclass:iter->second->type() == AnimatedNodeType::Valueso an addition or interpolation node was rejected with
"does not exist, or is not a 'value' node", even thoughstatic_cast<ValueAnimatedNode*>would have been valid. Replaced with an exhaustiveisValueNodeTypepredicate, so every node type whose C++ class derives fromValueAnimatedNode(includingRound) is accepted and the non-value ones are still rejected.AnimatedNode.__onAnimatedValueUpdateReceivedhad to accept a missingoffsetas well: the C++ backend emitsonAnimatedValueUpdatewithout one, which would otherwise produceNaN(value + undefined).Changelog:
[GENERAL] [FIXED] -
addListenerfires again for natively drivenAnimatedvalues derived withadd/subtract/multiply/divide/modulo/diffClamp/interpolateTest Plan:
New tests
Libraries/Animated/__tests__/AnimatedComposition-itest.js— renders a derived node bound totranslateX, attaches a listener to the derived node before it is made native, runs atiminganimation on the source value, and asserts the listener observed the derived value; then assertsremoveListenerstops the updates. Parameterised over the five operators plusinterpolate, and over both drivers.ReactCommon/react/renderer/animated/tests/AnimatedNodeTests.cpp—StartListeningToDerivedValueNodeasserts the C++ backend delivers updates for anadditionnode and stops onstopListeningToAnimatedNodeValue.StartListeningToNonValueNodeIsIgnoredasserts registering on atransformnode is still a no-op.Results
Reverting only the fix (keeping the new test) fails exactly the six
native drivercases, which is the reported bug; theJS drivercases were never broken:No regressions in the rest of the Animated suite:
yarn build-typesregeneratedReactNativeApi.d.ts; the only substantive change is thatAnimatedValue'sremoveListener/removeAllListenersare now inherited fromAnimatedNoderather than redeclared.addListenerkeeps its narrowerValueListenerCallbacksignature.Correction to an earlier revision of this description: the C++ tests were not run locally, and they are not covered by the public CI either.
react/renderer/animated/testsis excluded from the iOS build (React-Fabric.podspec:ss.exclude_files = "react/renderer/animated/tests") and is not in the Android CMake glob (react/renderer/animated/CMakeLists.txtglobs*.cpp drivers/*.cpp event_drivers/*.cpp internal/*.cpp nodes/*.cpp), so they build only in the internal build reached at import time. What is verified here:NativeAnimatedNodesManager.cppcompiles clean under-Wall -Werror -Wpedanticas part of the Fantom tester build, and the JS-side changes are covered by the Fantom and Jest runs above.Notes
For a colour
interpolate()on the native driver the listener receives the interpolated colour as a number rather than a string, because the value is computed natively. That matches pre-0.78 behaviour and is out of scope here.