Description
Hi Software Mansion team ! I've been trying out V3 on our SDK and it works really well, however I believe something got broken with the base components (at least the ones I looked at).
In react-native-gesture-handler@3.0.0, wrapping the new v3 composite components (Pressable, Touchable and likely the other v3 components routed through NativeDetector) with Animated.createAnimatedComponent from react-native-reanimated silently drops nearly every useAnimatedStyle style write applied to that wrapper. Empirically the only animated style prop that survives is transform every other prop I tested (width, height, opacity, …) is dropped.
The same useAnimatedStyle on the legacy LegacyPressable reexport or on a plain Animated.View,behaves correctly under identical setup. This isolates the regression to the v3 detector + composite component path (I believe).
Downgrading to react-native-gesture-handler@2.31.2 solves the issue of course.
Reproducer
Minimal reproduction repo: https://github.com/isekovanic/RNGH3LayoutAnimBug
The repro renders six ReanimatedSwipeable rows in a ScrollView and each row's right action is a colored "slot" containing an animated button driven from the swipeable's appliedTranslation SharedValue. The animated style differs per row to isolate which props survive the v3 path:
- Row 1:
createAnimatedComponent(Pressable) - v3, width + opacity - BROKEN (red button width stays 0)
- Row 2:
createAnimatedComponent(Touchable) - v3, width + opacity - BROKEN (red button width stays 0)
- Row 3:
createAnimatedComponent(LegacyPressable) - v2, width + opacity - WORKS (red button grows 0 -> 80)
- Row 4:
createAnimatedComponent(Pressable) - v3, transform-only (scale) - WORKS (the only animated prop that survives)
- Row 5:
createAnimatedComponent(Pressable) - v3, transform + opacity - PARTIAL (scale animates, opacity does not)
- Row 6:
Animated.View - baseline width+opacity+scale - WORKS (all three apply; isolates the regression to the v3 detector path)
Due to rows 4 and 6 together I'm under the impression that the worklet, viewTag wiring and Reanimated commit hook are healthy. Row 5 is the dispositive case: transform.scale and opacity written from the same useAnimatedStyle worklet on the same element - one applies, the other doesn't.
I did not test any other components than the touchables/Pressable specifically.
Why disable FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS ?
This one is disabled because of the fact that the forced React render after the animation settles actually fixes the bug (it will make the item appear), since I'm guessing it reconciles the ShadowNode state under the hood and applies the correct styles. While animating however this of course does not work still.
After a bunch of debugging here's what I was able to find:
- I believe that there's a race condition somewhere, perhaps related to the rerenders that happen during animation bookkeeping that somehow get the internal
ShadowNode and the actual style out of sync (the animation runs and all UI thread values are updated, but the ShadowNode is not aware of this)
- This is way more obvious by enabling
FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS and seeing that after the animation settles, after like 100ms or so the content actually does appear since the rerender at the end of it reconciles everything; which is why I disabled it for the repro
- In other words, the first value is read, snapshotted and used as a stale value for each
ShadowNode update that happens under the hood (for example width: 0 for some of the rows reproducing this and opacity: 0.15 for some of the others)
- Reanimated's UI thread worklet writes (
global._updateProps -> Fabric shadow tree mutation) appear to be intercepted, dropped or reverted somewhere in the RNGestureHandlerDetectorNativeComponent native implementation when the target view is a descendant of HostGestureDetector in the v3 path. I think that transform survives because it's typically committed to the layer's transform matrix on a separate path that bypasses whatever step is dropping the other writes
- Removing the
GestureDetector here entirely or simply passing some random gesture not based on the hook API (i.e new NativeGesture() for example) fixes the issue
- Other (failed) experiments:
- Condensing the
gesture used to something really simple (it's not any of the specific composite gestures that do this) while still using the hook API
- Stripping all other props did not really do anything
- Ref forwarding for
const Pressable = ({ ref, ...rest }) and emit ref={ref} explicitly on <PureNativeButton>
- Reanimated version. Reproduced on both
4.4.0 + worklets 0.9.1 and 4.3.1 + worklets 0.8.3, so doesn't seem like it's an issue in react-native-reanimated
Expected behavior
Wrapping a v3 Pressable/Touchable with createAnimatedComponent should apply every useAnimatedStyle style write the same way it does on Animated.View or the legacy v2 components.
Actual behavior
Every useAnimatedStyle style prop except transform is silently dropped - no warning, no error, the worklet still fires, the legacy and Animated.View controls in the same render confirm the rest of the plumbing is alive.
Workaround
In case anyone else needs this and as mentioned in the repro, avoiding to wrap v3 composite components with createAnimatedComponent and putting the animated style on an Animated.View wrapper and using a non-animated Pressable / Touchable as a positioned child works just fine:
<Animated.View style={[styles.action, animatedStyle]}>
<Pressable onPress={onPress} style={StyleSheet.absoluteFill}>
{/* content */}
</Pressable>
</Animated.View>
Animated.View writes go through the well tested fast path and bypass the v3 detector entirely (see row 6 of the repro).
Steps to reproduce
git clone https://github.com/isekovanic/RNGH3LayoutAnimBug
- Run
npm install and npx pod-install
- Run the app on either
ios or android
- Try to swipe all rows
- Observe how the rows that wrap the new
Pressable/Touchable components do not work for anything other than transform, while the legacy and baseline one work just fine
A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://github.com/isekovanic/RNGH3LayoutAnimBug
Gesture Handler version
3.0.0
React Native version
0.85.3
Platforms
iOS, Android
JavaScript runtime
Hermes
Workflow
React Native (without Expo)
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
Reproduced on iPhone 12 Pro and Xiaomi Redmi Note 13, but I don't think this is related as it's reproducible pretty much on any device.
Acknowledgements
Yes
Description
Hi Software Mansion team ! I've been trying out V3 on our SDK and it works really well, however I believe something got broken with the base components (at least the ones I looked at).
In
react-native-gesture-handler@3.0.0, wrapping the new v3 composite components (Pressable,Touchableand likely the other v3 components routed throughNativeDetector) withAnimated.createAnimatedComponentfromreact-native-reanimatedsilently drops nearly everyuseAnimatedStylestyle write applied to that wrapper. Empirically the only animated style prop that survives istransformevery other prop I tested (width,height,opacity, …) is dropped.The same
useAnimatedStyleon the legacyLegacyPressablereexport or on a plainAnimated.View,behaves correctly under identical setup. This isolates the regression to the v3 detector + composite component path (I believe).Downgrading to
react-native-gesture-handler@2.31.2solves the issue of course.Reproducer
Minimal reproduction repo: https://github.com/isekovanic/RNGH3LayoutAnimBug
The repro renders six
ReanimatedSwipeablerows in aScrollViewand each row's right action is a colored "slot" containing an animated button driven from the swipeable's appliedTranslationSharedValue. The animated style differs per row to isolate which props survive the v3 path:createAnimatedComponent(Pressable)- v3, width + opacity - BROKEN (red button width stays 0)createAnimatedComponent(Touchable)- v3, width + opacity - BROKEN (red button width stays 0)createAnimatedComponent(LegacyPressable)- v2, width + opacity - WORKS (red button grows 0 -> 80)createAnimatedComponent(Pressable)- v3, transform-only (scale) - WORKS (the only animated prop that survives)createAnimatedComponent(Pressable)- v3, transform + opacity - PARTIAL (scale animates, opacity does not)Animated.View- baseline width+opacity+scale - WORKS (all three apply; isolates the regression to the v3 detector path)Due to rows 4 and 6 together I'm under the impression that the worklet,
viewTagwiring and Reanimated commit hook are healthy. Row 5 is the dispositive case:transform.scaleandopacitywritten from the sameuseAnimatedStyleworklet on the same element - one applies, the other doesn't.I did not test any other components than the touchables/
Pressablespecifically.Why disable
FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS?This one is disabled because of the fact that the forced
Reactrender after the animation settles actually fixes the bug (it will make the item appear), since I'm guessing it reconciles theShadowNodestate under the hood and applies the correct styles. While animating however this of course does not work still.After a bunch of debugging here's what I was able to find:
ShadowNodeand the actual style out of sync (the animation runs and all UI thread values are updated, but theShadowNodeis not aware of this)FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONSand seeing that after the animation settles, after like 100ms or so the content actually does appear since the rerender at the end of it reconciles everything; which is why I disabled it for the reproShadowNodeupdate that happens under the hood (for examplewidth: 0for some of the rows reproducing this andopacity: 0.15for some of the others)global._updateProps->Fabricshadow tree mutation) appear to be intercepted, dropped or reverted somewhere in theRNGestureHandlerDetectorNativeComponentnative implementation when the target view is a descendant ofHostGestureDetectorin the v3 path. I think thattransformsurvives because it's typically committed to the layer's transform matrix on a separate path that bypasses whatever step is dropping the other writesGestureDetectorhere entirely or simply passing some random gesture not based on the hook API (i.enew NativeGesture()for example) fixes the issuegestureused to something really simple (it's not any of the specific composite gestures that do this) while still using the hook APIconst Pressable = ({ ref, ...rest })and emitref={ref}explicitly on<PureNativeButton>4.4.0+ worklets0.9.1and4.3.1+ worklets0.8.3, so doesn't seem like it's an issue inreact-native-reanimatedExpected behavior
Wrapping a v3
Pressable/TouchablewithcreateAnimatedComponentshould apply everyuseAnimatedStylestyle write the same way it does onAnimated.Viewor the legacy v2 components.Actual behavior
Every
useAnimatedStylestyle prop excepttransformis silently dropped - no warning, no error, the worklet still fires, the legacy andAnimated.Viewcontrols in the same render confirm the rest of the plumbing is alive.Workaround
In case anyone else needs this and as mentioned in the repro, avoiding to wrap v3 composite components with
createAnimatedComponentand putting the animated style on anAnimated.Viewwrapper and using a non-animatedPressable/Touchableas a positioned child works just fine:Animated.Viewwrites go through the well tested fast path and bypass the v3 detector entirely (see row 6 of the repro).Steps to reproduce
git clone https://github.com/isekovanic/RNGH3LayoutAnimBugnpm installandnpx pod-installiosorandroidPressable/Touchablecomponents do not work for anything other thantransform, while the legacy and baseline one work just fineA link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://github.com/isekovanic/RNGH3LayoutAnimBug
Gesture Handler version
3.0.0
React Native version
0.85.3
Platforms
iOS, Android
JavaScript runtime
Hermes
Workflow
React Native (without Expo)
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
Reproduced on
iPhone 12 ProandXiaomi Redmi Note 13, but I don't think this is related as it's reproducible pretty much on any device.Acknowledgements
Yes