react-native-reanimated
react-native-reanimated copied to clipboard
`useAnimatedStyle` causing focus and button press issues in `FlatList`
Description
I am encountering a problem when using animations with FlatList. Specifically, when I use Animated or useAnimatedStyle from Reanimated for tab animations, the following issues occur:
The TextInput in the second tab (and onwards) doesn't gain focus when tapped. Any TouchableOpacity or button inside the FlatList items also becomes unresponsive. When I switched from Reanimated's useAnimatedStyle to Animated from React Native, the issue was resolved, and everything worked as expected. This appears to be a problem with how useAnimatedStyle interacts with FlatList.
Steps to reproduce
Copy the code below into a fresh React Native project. Run the app. Try focusing on the TextInput in the second tab or pressing any button inside the second tab. Comment out Reanimated usage and use React Native's Animated instead to observe the difference.
import React, { useState, useRef } from 'react';
import {
View,
TextInput,
FlatList,
StyleSheet,
Dimensions,
Animated,
TouchableOpacity,
} from 'react-native';
import { Box, Typography } from '@shopify/restyle';
const { width } = Dimensions.get('window');
const spacing = 16;
type TabData = {
id: number;
title: string;
placeholder: string;
};
const CustomTabView = () => {
const [tabsData] = useState<TabData[]>([
{ id: 1, title: 'Tab 1', placeholder: 'Enter text for Tab 1' },
{ id: 2, title: 'Tab 2', placeholder: 'Enter text for Tab 2' },
{ id: 3, title: 'Tab 3', placeholder: 'Enter text for Tab 3' },
]);
const [tabValues, setTabValues] = useState<{ [key: number]: string }>({
1: '',
2: '',
3: '',
});
const [activeTab, setActiveTab] = useState(0);
const flatListRef = useRef<FlatList>(null);
const animatedIndicator = useRef(new Animated.Value(0)).current;
const handleTabPress = (index: number) => {
setActiveTab(index);
// Animate the indicator to the selected tab
Animated.timing(animatedIndicator, {
toValue: index * (width / tabsData.length),
duration: 250,
useNativeDriver: false,
}).start();
flatListRef.current?.scrollToIndex({
animated: true,
index,
});
};
const handleInputChange = (id: number, value: string) => {
setTabValues((prevState) => ({ ...prevState, [id]: value }));
};
return (
<View style={styles.container}>
{/* Tab Buttons with Animated Indicator */}
<Box
flexDirection="row"
justifyContent="space-around"
bg="gray200"
position="absolute"
width={width - spacing}
top={0}
zIndex={1}
borderRadius="s"
overflow="hidden"
style={{
padding: spacing / 3,
marginHorizontal: spacing / 2,
}}
>
<Animated.View
style={[
styles.indicator,
{
width: width / tabsData.length,
transform: [{ translateX: animatedIndicator }],
},
]}
/>
{tabsData.map((tab, index) => (
<TouchableOpacity
key={index}
style={styles.tabButton}
onPress={() => handleTabPress(index)}
>
<Typography
textAlign="center"
fontFamily="medium"
color={activeTab === index ? 'globalWhite' : 'textSecondary'}
>
{tab.title}
</Typography>
</TouchableOpacity>
))}
</Box>
{/* Content for Each Tab */}
<FlatList
ref={flatListRef}
data={tabsData}
renderItem={({ item }) => (
<View style={styles.tabContainer} key={item.id}>
<TextInput
style={styles.textInput}
placeholder={item.placeholder}
value={tabValues[item.id]}
onChangeText={(text) => handleInputChange(item.id, text)}
onFocus={() => console.log(`Focused on Tab ${item.id}`)}
/>
</View>
)}
keyExtractor={(item) => item.id.toString()}
horizontal
showsHorizontalScrollIndicator={false}
pagingEnabled
keyboardShouldPersistTaps="always"
removeClippedSubviews={false} // Important to ensure inputs offscreen remain interactive
onMomentumScrollEnd={(event) => {
const index = Math.floor(event.nativeEvent.contentOffset.x / width);
setActiveTab(index);
Animated.timing(animatedIndicator, {
toValue: index * (width / tabsData.length),
duration: 250,
useNativeDriver: false,
}).start();
}}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginTop: 100, // Space for tab bar at the top
},
tabContainer: {
width,
justifyContent: 'center',
alignItems: 'center',
},
textInput: {
width: '80%',
borderColor: '#ccc',
borderWidth: 1,
padding: 10,
borderRadius: 5,
},
tabButton: {
flex: 1,
alignItems: 'center',
},
indicator: {
position: 'absolute',
top: 0,
bottom: 0,
backgroundColor: 'blue',
borderRadius: 10,
},
});
export default CustomTabView;
Snack or a link to a repository
https://snack.expo.dev/_cgYFhPQvKDe1htoimwii
Reanimated version
3.16.1
React Native version
0.76
Platforms
Android
JavaScript runtime
None
Workflow
None
Architecture
None
Build type
None
Device
None
Device model
No response
Acknowledgements
Yes
I've got the same problem, any update on it ?
Same
Same issue
Same issue, some button unresponsive, Even not in the same screen.
Same issue guys ! I can help to reproduce it also cause issues on Scrollview component.
hi @ravis-farooq, I ran the provided snack (without shopify components, due to some problems with them), and text inputs focus and work. Do you still have the same problem? Is the tab animation the issue? Maybe you can provide me with a video or another snakc for better understanding of the problem c:
https://github.com/user-attachments/assets/e8301ae4-6e2a-4557-a74c-7a374f76c82a
Hello, I can help reproduce this issue.
It appears to work fine on an emulator, but I discovered the problem when testing on a physical device. I’ve tested this on multiple Android devices running different versions of Android, and the issue persists.
To clarify, I used the TouchableOpacity component from React Native. It worked well when I wasn’t scrolling, but during scrolling (around 20% of the scroll position), the button stopped functioning properly. The animation of the button still triggered, but no functions were called.
I found a workaround by using the Pressable component from react-native-gesture-handler, which resolved the issue for me.
Here is a video of the issue:
https://github.com/user-attachments/assets/6e20fff6-ea5d-4145-943a-c7d6e8ffc95f
I've used the following versions for testing:
"react-native": "0.77.0""react-native-reanimated": "4.0.0-nightly-20250122"
However, the issue persists even with the latest stable release:
"react-native-reanimated": "3.16.7""react-native": "0.76.6"
I think this issue is related also.
Here is a part of my code also :
const imageAnimatedStyle = useAnimatedStyle(() => {
return {
height: interpolate(
scrollHandlerOffset.value,
[-height / 2, 0, height / 3],
[IMG_HEIGHT * 2, IMG_HEIGHT, IMG_HEIGHT / 1.5],
'clamp',
),
};
});
return (
<CustomSafeAreaView shouldNotUseTopInsets style={styles.container}>
<Animated.View style={[{width: width}, imageAnimatedStyle]}>
{loading ? (
<>
<SkeletonView style={styles.skeletonimage} />
<HeaderRecipe
data={data}
loading={loading}
handleShare={handleShare}
optimisticFavorited={like}
setOptimisticLike={setOptimisticLike}
isEvent={navigation.getParent()?.getState().index === 3}
/>
</>
) : (
<CustomBackgroundImage
key={data?.image}
style={styles.image}
resizeMode="cover"
source={{
uri: data?.image,
}}
fallbackProps={{
resizeMode: 'contain',
source: require('@assets/images/Plate.png'),
}}>
<View style={styles.layer}>
<HeaderRecipe
data={data}
loading={loading}
handleShare={handleShare}
optimisticFavorited={like}
setOptimisticLike={setOptimisticLike}
isEvent={navigation.getParent()?.getState().index === 3}
/>
<View style={styles.titlecontainer}>
<CustomText
numberOfLines={2}
testID="headertitle"
style={styles.title}>
{loading ? t('recipeDetails.loading') : data?.title}
</CustomText>
<View style={styles.timercontainer}>
<Ionicons size={25} color={WHITE} name={'stopwatch'} />
<CustomText style={styles.timertext}>
{loading
? t('SignUpScreen.LoadingUsernameCheck')
: getTime(data?.totalTime ?? 0)}
</CustomText>
</View>
</View>
</View>
</CustomBackgroundImage>
)}
</Animated.View>
<View style={[styles.borderview, {backgroundColor: colors.background}]} />
<Animated.ScrollView
ref={scrollRef}
scrollEventThrottle={5}
scrollEnabled={!loading}
showsVerticalScrollIndicator={!loading}
contentContainerStyle={{
paddingBottom: 20,
paddingHorizontal: 15,
}}>
<RecipeActionsView
recipeId={data?.id}
disabled={loading || !data}
onPressLiveRecipe={RequestCameraPermission}
/>
<View style={[styles.recipecontainer, {borderColor: colors.border}]}>
<CustomText
style={[
styles.subtitle,
{
width: width - 150,
color: colors.primary,
borderBottomColor: colors.primary,
},
]}>
{t('recipeDetails.ingredients')}
</CustomText>
<View style={styles.modifyquantitycontainer}>
{loading ? (
<SkeletonView style={styles.modifyquantityskeleton} />
) : (
<ModifyQuantityButton
style={styles.modifyquantity}
quantity={numberOfPeople}
onPressLess={() => modifyItemQuantity(numberOfPeople - 1)}
onPressMore={() => modifyItemQuantity(numberOfPeople + 1)}
unit={
numberOfPeople === 1
? t('recipeDetails.person')
: t('recipeDetails.people')
}
/>
)}
</View>
</View>
</Animated.ScrollView>
</CustomSafeAreaView>
);
I would also like to mention that switching to ScrollView from React Native did not resolve the issue. The only effective solution I found, without using the Pressable component from react-native-gesture-handler, was to replace the Animated.View with a simple View.
Hi @mateoabrbt, I put some items inside Animated.ScrollView, and also modifyQuantityButtons inside ScrollView as well. I tested either Button component and TouchableOpacity, and both of them work when scrolled. 😅 I'll attach my minimal reproduction of your code
https://github.com/user-attachments/assets/66e056c5-b89d-449e-815b-8f2874e38e1a
https://github.com/user-attachments/assets/2e02cf8c-0f6b-4c00-9c2e-4f1aee72434c
code
import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Animated from 'react-native-reanimated';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function App() {
const [quantity, setQuantity] = useState(2);
const scrollRef = useRef(null);
const modifyItemQuantity = (quantity) => {
if (quantity > 0) setQuantity(quantity);
};
return (
<SafeAreaView style={styles.container}>
<Animated.ScrollView
ref={scrollRef}
scrollEventThrottle={5}
contentContainerStyle={styles.scrollContentContainer}>
<View style={styles.recipeContainer}>
<Text style={styles.subtitle}>Ingredients</Text>
<View style={styles.modifyQuantityContainer}>
{/* <View style={styles.modifyQuantityButtons}>
<Button
onPress={() => modifyItemQuantity(quantity - 1)}
title="Minus"
/>
<Text style={styles.quantityText}>{quantity}</Text>
<Button
title="Plus"
onPress={() => modifyItemQuantity(quantity + 1)}
/>
</View> */}
<View style={styles.modifyQuantityButtons}>
<TouchableOpacity
onPress={() => modifyItemQuantity(quantity - 1)}>
<Text
style={{
borderColor: 'red',
borderWidth: 2,
padding: 8,
backgroundColor: 'pink',
margin: 8,
}}>
Minus
</Text>
</TouchableOpacity>
<Text style={styles.quantityText}>{quantity}</Text>
<TouchableOpacity
onPress={() => modifyItemQuantity(quantity + 1)}>
<Text
style={{
borderColor: 'green',
borderWidth: 2,
padding: 8,
backgroundColor: 'palegreen',
margin: 8,
}}>
Plus
</Text>
</TouchableOpacity>
</View>
</View>
</View>
{Array.from({ length: 20 }).map((_, index) => (
<View key={index} style={styles.scrollItem}>
<Text style={styles.scrollItemText}>Item {index + 1}</Text>
</View>
))}
</Animated.ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
layer: {
flex: 1,
justifyContent: 'flex-end',
padding: 16,
},
scrollContentContainer: {
padding: 16,
},
recipeContainer: {
padding: 16,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
},
subtitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
},
modifyQuantityContainer: {
flexDirection: 'row',
alignItems: 'center',
},
modifyQuantityButtons: {
flexDirection: 'row',
alignItems: 'center',
},
button: {
fontSize: 24,
color: '#007BFF',
marginHorizontal: 8,
},
quantityText: {
fontSize: 18,
fontWeight: 'bold',
},
scrollItem: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ddd',
},
scrollItemText: {
fontSize: 16,
},
});
Hello @patrycjakalinska ! That’s interesting, but I believe I know why it didn’t work as expected. The issue with the code I sent wasn’t related to the Animated.ScrollView itself but rather to the Animated.View I added on top of the ScrollView to create the effect of the image shrinking in height before the scroll starts. When I removed that part, everything worked perfectly without any issues. However, adding the Animated.View—even with no children inside—caused the bug.
const imageAnimatedStyle = useAnimatedStyle(() => {
return {
height: interpolate(
scrollHandlerOffset.value,
[-height / 2, 0, height / 3],
[IMG_HEIGHT * 2, IMG_HEIGHT, IMG_HEIGHT / 1.5],
'clamp',
),
};
});
return (
<CustomSafeAreaView shouldNotUseTopInsets style={styles.container}>
<Animated.View style={[{width: width}, imageAnimatedStyle]}>
{children}
</Animated.View>
</CustomSafeAreaView>
);
I found the problem. The animation caused the click event of the internal element of Animated.View to be abnormal.
Encapsulates a hook to obtain the translate style of the current panel
import { useEffect } from 'react';
import {
Easing,
WithTimingConfig,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
const DEFAULT_TIME_CONFIG: WithTimingConfig = {
duration: 300,
easing: Easing.out(Easing.ease),
};
export default function useTranslateY(
visible?: boolean,
options?: {
timeConfig?: WithTimingConfig;
onComplete?: (value: number) => void;
},
) {
const translateY = useSharedValue(100);
const timeConfig = options?.timeConfig || DEFAULT_TIME_CONFIG;
useEffect(() => {
if (visible) {
translateY.value = withTiming(0, timeConfig, () =>
options?.onComplete?.(0),
);
} else {
translateY.value = withTiming(100, timeConfig, () =>
options?.onComplete?.(100),
);
}
}, [options, timeConfig, translateY, visible]);
const animatedTranslateY = useAnimatedStyle(() => ({
transform: [{ translateY: `${translateY.value}%` }], // effect press event, all of press event and touch event dont work, event animation done
// transform: [{ translateY: visible ? 0 : `${translateY.value}%` }], // it work without animation
}));
return animatedTranslateY;
}
In Android, the Panel child elements cannot trigger press and touch events normally, but there seems to be no problem in iOS.
const PanelBase: React.FC<PropsWithChildren<IPanelBaseProps>> = ({
visible,
maxHeightPercent = 0.7,
minHeightPercent = 0.3,
children,
containerStyle,
...modalBaseProps
}) => {
const insets = useSafeAreaInsets();
const animatedTranslateY = useTranslateY(visible);
const heightStyle = useMemo(() => {
let maxHeight = SCREEN_HEIGHT * maxHeightPercent + insets.bottom;
if (maxHeight > SCREEN_HEIGHT) {
maxHeight = SCREEN_HEIGHT;
}
let minHeight = SCREEN_HEIGHT * minHeightPercent + insets.bottom;
if (minHeight > SCREEN_HEIGHT) {
minHeight = SCREEN_HEIGHT;
}
if (minHeight > maxHeight) {
minHeight = maxHeight;
}
return { maxHeight, minHeight };
}, [insets.bottom, maxHeightPercent, minHeightPercent]);
return (
<ModalBase visible={visible} {...modalBaseProps}>
<Animated.View style={[styles.content, animatedTranslateY]}>
<View
style={[
heightStyle,
{ paddingBottom: insets.bottom || px2dp(16) },
containerStyle,
]}>
{children}
</View>
</Animated.View>
</ModalBase>
);
};
I see thanks for that update !
Hi everyone, please check if the issue persists if following feature flags are added inside /android/gradle.propertites:
updateRuntimeShadowNodeReferencesOnCommit=true
useShadowNodeStateOnClone=true
If you want to dig more into this feature flags you can go to the related PR. Also make sure you are using RN version that enabled this flags.
The issue you're encountering might be from React Native itself. If it's true, it appears to be caused by outdated state being cloned, which results in an incorrect hitbox being applied to Pressable components - you can read more about it in this issue (big kudos to @bartlomiejbloniarz ).
Please let me know if it helped!
@patrycjakalinska
This fix does not work for us in all cases on Android using Animated.ScrollView. We switched to 0.79.2, enabled those flags, but after scroll (we also transform ScrollView translateY with useAnimatedStyle in this case to move it up which is actually breaking taps) we still don't receive onPress events on some elements but receive onPressIn and onPressOut.
Same issue, any update on this ?
I also ran into another similar issue. I was using Animated.View from react-native-reanimated, and my Raw Bottom Sheet wasn’t working properly — the gestures and animations were buggy or didn’t trigger at all.
After some debugging, I switched to Animated.View from react-native instead, and that resolved the issue. If you're using reanimated, it might be worth checking if it's interfering with gesture handling or the sheet’s rendering.
Hope this helps someone else facing the same problem!
Update Reanimated to 4.1.0 with RN 0.79 with the two flags fixed the issue on my side!