Held animated opacity reverts to initial value for 2–3 frames during a navigator swap with USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS enabled (default since 4.3.0)
Description
A full-screen cover Animated.View whose opacity is driven by useAnimatedStyle + withTiming(1) and then held static (no running animation) reverts to opacity 0 for 2–3 frames while a large tree change happens beneath it, then snaps back to 1. This happens with USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS at its default (true, defaulted since 4.3.0). Setting it to false (the pre-4.3.0 default) eliminates the glitch.
Setup where we hit it (auth sign-out transition in a production app):
- A cover overlay is mounted as a sibling above the app tree:
useAnimatedStyleopacity,withTiming(1, { duration: 180 }), then it just sits there fully opaque ("hold" phase) while sign-out work runs underneath. - Under the cover, the auth flip unmounts an entire
react-native-screensnative stack and mounts a different one (via expo-router), and a keyed provider remounts a large subtree — i.e. one very large React commit, followed by the new stack settling. - ~0.6–0.8s into the hold, the cover's on-screen opacity flips
1 → 0for exactly 2–3 frames (~25–40ms), fully exposing the freshly mounted screen beneath, then flips back to1. The revert is binary — no intermediate opacity values — and the shared value is1the whole time.
Frame-by-frame captures (per-frame mean-luma analysis of simulator recordings) show the same signature across runs: smooth 180ms fade-in → opaque hold → single binary 2–3-frame punch-through ~0.6–0.8s later → opaque again → smooth fade-out. So a single commit paints the view with the React-side props (opacity 0) and the registry value is re-applied a couple of frames later.
Feature-flag bisect (each configuration verified in the built binary via strings on the compiled REANIMATED_FEATURE_FLAGS define)
FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS |
USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS |
Result |
|---|---|---|
true (default) |
true (default) |
reproduces |
false |
true (default) |
still reproduces |
false |
false |
fixed — no glitch |
The second row rules out the settled-props GC family (#9478, #9965, fixed by #9527): with FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS:false the registry entry is never synced/evicted, yet the stomp still occurs. The third row points at ReanimatedCommitHook skipping a non-React commit (plausibly a react-native-screens state progression while the newly mounted stack settles): that commit carries the React-side opacity (0, since the 180ms entrance settled well under the sync threshold, so React never learned the animated value), and nothing re-merges the registry value into it.
Possibly related: #9111 (commit pausing), #7728 (external commit reverts animated view), #9614 (same symptom class, navigation-triggered).
Steps to reproduce
- iOS, New Architecture, Release build (we could not reproduce reliably in Debug), default feature flags.
- Mount a full-screen sibling overlay above a navigator tree; fade it in with
withTiming(1, { duration: ~200 })and hold it (no further animation). - ~200ms after the entrance settles, perform a large swap beneath it: unmount an entire
react-native-screensnative stack, mount a different one, and remount a keyed subtree in the same update. - Record the screen and step frames: within ~1s of the swap the overlay drops to opacity 0 for 2–3 frames.
Minimal sketch of the pattern (illustrative, not our production code):
function Cover() {
const opacity = useSharedValue(0)
useEffect(() => {
opacity.set(withTiming(1, { duration: 180 }))
}, [])
const style = useAnimatedStyle(() => ({ opacity: opacity.value }))
return <Animated.View pointerEvents="auto" style={[StyleSheet.absoluteFill, { backgroundColor: "black", zIndex: 9999 }, style]} />
}
// App renders <Cover /> as a sibling ABOVE this:
// {loggedIn ? <StackA /> : <StackB />} // native-stack navigators
// wrapped in a provider keyed by the user id, so the flip also remounts the subtree.
// Flip `loggedIn` ~200ms after the cover's entrance completes.We can invest in a standalone repro repository if this report plus the flag bisect isn't enough to locate it — the bisect is deterministic on our app (multiple recorded runs per configuration, binary-verified flags).
Snack or a link to a repository
None yet (see above — happy to build one if needed).
Reanimated version
4.3.1
Worklets version
0.8.3
React Native version
0.85.3
Platforms
iOS (observed; Android untested)
JavaScript runtime
Hermes
Workflow
Expo (SDK 56, expo-router ~56.2.11, react-native-screens 4.25.2)
Architecture
Fabric (New Architecture)
Reanimated feature flags
See bisect table above; defaults otherwise (DISABLE_COMMIT_PAUSING_MECHANISM:false, USE_SYNCHRONIZABLE_FOR_MUTABLES:true).
React Native release level
Stable
Build type
Release app build
Device
iOS Simulator (iPhone 17 Pro)
Host machine
macOS (Apple Silicon)
Source: software-mansion/react-native-reanimated