Rashed Mohammadi
Rashed Mohammadi

Reputation: 11

TouchableOpacity not working properly in nested FlatList or ScrollView

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.

What I’ve Tried:

Despite all these attempts, the touch events are still inconsistent. Any help would be greatly appreciated!

Upvotes: 0

Views: 16

Answers (1)

Umang Thakkar
Umang Thakkar

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

Related Questions