Reputation: 11
I need to display product categories. Each category contains multiple products that should scroll horizontally.
When I click on a category item, the touch event doesn’t work consistently. Sometimes it works after multiple taps, and sometimes it doesn’t work at all. This issue only occurs when using ScrollView or FlatList. If I don’t use these components, the touch events work fine.
This my code
import React, {useState, useEffect, useCallback, memo} from "react";
import {
View,
Text,
StyleSheet,
SafeAreaView,
Image,
StatusBar,
TouchableOpacity,
ScrollView,
} from "react-native";
import Colors from "@/constants/Colors";
import {responsiveFontSize} from "@/utils/Responsive";
import {useRouter} from "expo-router";
import BannerSlider from "@/components/home/BannerSlider";
import BannerSkeleton from "@/components/skeleton/home/bannerSkeleton";
import CategorySkeleton from "@/components/skeleton/home/categorySkeleton";
import Sidebar from "@/components/Sidebar";
import {MenuIcon} from "@/assets/icons/index";
import {useAuth} from "@/context/AuthContext";
import {fetchAllBanners, fetchCategoryList} from "@/lib/api";
interface Category {
categoryId: string;
name: string;
image?: string;
parentCategoryId: string | null;
}
interface Banner {
id: string;
image: string;
}
const CategoryItem: React.FC<{
item: Category;
onPress: (categoryId: string) => void;
}> = memo(({item, onPress}) => {
return (
<TouchableOpacity
activeOpacity={1}
onPress={() => onPress(item.categoryId)}
>
<View
style={{
width: responsiveFontSize(28),
height: responsiveFontSize(34),
}}
>
<View style={styles.categoryItem}>
{item.image ? (
<Image
source={{uri: item.image}}
style={styles.categoryImage}
resizeMode="contain"
/>
) : (
<View style={styles.categoryImagePlaceholder} />
)}
</View>
<Text style={styles.categoryTitle}>{item.name}</Text>
</View>
</TouchableOpacity>
);
});
const HomeUserScreen: React.FC = () => {
const router = useRouter();
const {userRole} = useAuth();
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
const [allBanners, setAllBanners] = useState<Banner[]>([]);
const [isFetchingBanners, setIsFetchingBanners] = useState<boolean>(false);
const [isFetchingCategories, setIsFetchingCategories] =
useState<boolean>(false);
const [categories, setCategories] = useState<Category[]>([]);
useEffect(() => {
const fetchBanners = async () => {
setIsFetchingBanners(true);
try {
const bannersResponse = await fetchAllBanners();
setAllBanners(bannersResponse.data);
} catch (error) {
console.error("Error fetching banners:", error);
} finally {
setIsFetchingBanners(false);
}
};
fetchBanners();
}, []);
useEffect(() => {
const fetchCategories = async () => {
setIsFetchingCategories(true);
try {
const categoriesResponse = await fetchCategoryList();
setCategories(categoriesResponse.data);
categoriesResponse.data.forEach((category: Category) => {
if (category.image) {
Image.prefetch(category.image);
}
});
} catch (error) {
console.error("Error fetching categories:", error);
} finally {
setIsFetchingCategories(false);
}
};
fetchCategories();
}, []);
const navigateToProduct = useCallback(
(categoryId: string) => {
router.push({pathname: "/product/[id]", params: {id: categoryId}});
},
[router]
);
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="light-content" backgroundColor={Colors.primary} />
<Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
<View style={styles.header}>
<TouchableOpacity
onPress={() => setIsSidebarOpen(true)}
style={styles.menuButton}
>
<MenuIcon />
</TouchableOpacity>
<Text style={styles.headerTitle}>خانه</Text>
</View>
<>
{isFetchingBanners ? (
<BannerSkeleton />
) : (
<BannerSlider data={allBanners} />
)}
{isFetchingCategories ? (
<CategorySkeleton />
) : (
<ScrollView
style={styles.pageContainer}
showsVerticalScrollIndicator={false}
>
{categories
.filter((category) => category.parentCategoryId === null)
.map((category) => (
<View key={category.categoryId} style={styles.categorySection}>
<Text style={styles.categorySectionTitle}>
{category.name}
</Text>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.horizontalScroll}
showsVerticalScrollIndicator={false}
>
{categories
.filter(
(subcategory) =>
subcategory.parentCategoryId === category.categoryId
)
.map((subcategory) => (
<CategoryItem
key={subcategory.categoryId}
item={subcategory}
onPress={navigateToProduct}
/>
))}
</ScrollView>
</View>
))}
</ScrollView>
)}
</>
</SafeAreaView>
);
};
I tried using ScrollView
for vertical scrolling and FlatList
or ScrollView
for horizontal scrolling. However, I’m facing issues with touch events (TouchableOpacity
) not working properly. The touch events are inconsistent—sometimes they work after multiple taps, and sometimes they don’t work at all.
hitSlop
: I experimented with different values for hitSlop
to increase the touchable area, but it didn’t resolve the issue.
activeOpacity
: I tried different values for activeOpacity
, including 1
, but the problem persists.
TouchableWithoutFeedback
and Pressable
: I replaced TouchableOpacity
with TouchableWithoutFeedback
and Pressable
, but the touch events still don’t work reliably.
keyboardShouldPersistTaps
in ScrollView
: I set keyboardShouldPersistTaps
to both handled
and always
, but it didn’t fix the issue.
Debugging categoryId
and expo-router
: I confirmed that the issue is not related to categoryId
or expo-router
, as the problem only occurs when using ScrollView
or FlatList
.
Despite all these attempts, the touch events are still inconsistent. Any help would be greatly appreciated!
Upvotes: 0
Views: 16
Reputation: 242
There is an open issue with TouchableOpacity
inside FlatList or ScrollView so I had to make my custom pressable component. I am attaching reference code below. it should work properly.
Note : It won't give you the press effect of TouchableOpacity
but It will work properly.
Code :
import React, { ReactNode } from "react";
import {
TapGestureHandler,
State,
TapGestureHandlerStateChangeEvent,
} from "react-native-gesture-handler";
import { View, StyleProp, ViewStyle } from "react-native";
interface CustomPressableProps {
onPress: () => void;
children: ReactNode;
style?: StyleProp<ViewStyle>;
disabled?: boolean;
}
const CustomPressable: React.FC<NRPressableProps> = ({
onPress,
children,
style,
disabled,
}) => {
const onHandlerStateChange = (event: TapGestureHandlerStateChangeEvent) => {
if (event.nativeEvent.state === State.END) {
onPress();
}
};
return (
<TapGestureHandler
onHandlerStateChange={onHandlerStateChange}
enabled={!disabled}
>
<View style={style}>{children}</View>
</TapGestureHandler>
);
};
export default CustomPressable;
Upvotes: 0