Skip to content

Fix Animated values going stale across Activity hide and reveal - #59716

Open
Hassan1305 wants to merge 1 commit into
react:mainfrom
Hassan1305:fix/activity-touchable-opacity-stuck
Open

Hassan1305 wants to merge 1 commit into
react:mainfrom
Hassan1305:fix/activity-touchable-opacity-stuck

Conversation

@Hassan1305

@Hassan1305 Hassan1305 commented Oct 31, 2026 •

Copy link
Copy Markdown

Summary:

Fixes #58800. A TouchableOpacity whose onPress hides its parent <Activity> comes back stuck at activeOpacity. There are two causes, both in Animated:

  1. A stopped native animation overwrites an explicitly assigned value. Activity hiding runs componentWillUnmount, where TouchableOpacity calls anim.resetAnimation() while the press-out animation is running. JS sets the value to 1, then native's asynchronous end result for the stopped animation (value: 0.2) overwrites it. JS now disagrees with native, and the next render commits 0.2. setValue() has the same race, with or without Activity. A stopped animation's end result no longer syncs the value after resetAnimation() or setValue().
  2. Updates are dropped while hidden. Hiding detaches the ref, which clears onUpdateRef, so an animation finishing while hidden does not schedule a render. A component that does not otherwise re-render on reveal (for example behind memo) keeps its last committed value. useAnimatedProps now schedules an update when revealed, using a layout effect because those only re-run for the same node on reveal. A ref callback would not work: inline refs re-run it on every render, which would loop.

Trace on iOS before the fix, from temporary logging of rendered opacity:

  • Plain Animated.View: r=0.2 HIDE ... pout:{"value":1,"finished":true} SHOW plainJS=1 r=1. It recovers when its parent re-renders it.
  • TouchableOpacity: r=0.2 HIDE ... SHOW r=0.2. The JS value itself is stale.

Changelog:

[GENERAL] [FIXED] - Fix Animated values going stale when an animation is stopped by setValue/resetAnimation or ends while hidden by Activity, which left TouchableOpacity stuck at activeOpacity

Test Plan:

New Jest tests fail without the fix and pass with it:

yarn test packages/react-native/src/private/animated/__tests__/AnimatedNative-test.js
  • does not let a stopped animation overwrite the value set by resetAnimation (without fix: expected 1, received 0.2)
  • does not let a stopped animation overwrite the value set by setValue (without fix: expected 0.5, received 0.2)
  • commits the latest value when revealed after animating while hidden (without fix: expected 1, received 0.2)

RNTester, iOS Simulator (iPhone 17 Pro, iOS 26.2), on main, using a temporary Playground with TouchableOpacity buttons that hide their own Activity, then show it again:

Before: both the plain and the memo-wrapped TouchableOpacity come back at about 0.2. After: both come back at full opacity.

RNTester, Android emulator (Medium Phone, API 36.1), same screen: the bug did not reproduce without the fix (hide confirmed, unfixed bundle confirmed), and buttons also come back at full opacity with it, so no regression there. I did not investigate why Android differs.

JavaScript CI, all passing:

node ./scripts/run-ci-javascript-tests.js --maxWorkers 4
yarn lint-markdown
yarn test-typescript-legacy
yarn build-types --skip-snapshot
yarn test-generated-typescript
yarn format-check-javascript
yarn fantom packages/react-native/Libraries/Animated

Not covered:

  • cxxNativeAnimatedEnabled (the OSS canary backend, forced on in Fantom): a memo-wrapped Animated.View that finishes animating while hidden still reveals with the old opacity in Fantom. There, native writes the animated value to the shadow tree and the JS re-render does not change props. That is a separate C++ path and this PR does not change it, so no Fantom test is included. Fantom cannot drive native animations with the flag off.

🤖 Generated with Claude Code
before

after

A stopped native animation reports its value asynchronously, which overwrote
the value assigned by resetAnimation() or setValue(). TouchableOpacity resets
its value in componentWillUnmount, which also runs when an Activity hides it,
so it came back at activeOpacity.

Updates are also dropped while hidden because the ref is detached, so a
component that does not otherwise re-render on reveal kept its last committed
value. Schedule an update when revealed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 Oct 31, 2026
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Oct 31, 2026

This branch has not been deployed

No deployments
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. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

<TouchableOpacity /> inside a hidden <Activity> reappears stuck at activeOpacity on iOS

1 participant

⌂ Proxy