1

I have two components: AllBuyersTab and TicketCheckedInTab. The TicketCheckedInTab works fine, but I’m experiencing scroll lag in AllBuyersTab. Can anyone suggest a solution?

I tried various solutions by using different props available for FlatList from the React Native docs, but they didn’t help. Sometimes I got blank row issues, which I fixed by assigning a fixed height and using getItemLayout. However, the scrolling issue still persists — and surprisingly, it only happens in AllBuyersTab, not in TicketCheckedInTab

import React, { useEffect, useState } from 'react';
import {
    CHECKIN_MESSAGES,
    ERROR_MESSAGE_OVERRIDE,
    EVENT_ERROR_MESSAGES,
} from '@/constants/messages';
import {
    View,
    Text,
    StyleSheet,
    TouchableOpacity,
    ScrollView,
    FlatList,
    Modal,
    ActivityIndicator,
} from 'react-native';
import { router, useLocalSearchParams } from 'expo-router';
import { useDataProvider } from '@/provider/DataProvider';
import {
    getAttendeesTicketStatus,
    updateCancelledAttendeeTicketStatus,
    incrementCheckedCount,
    cleanLocalDB,
    getLastSyncAt,
    insertAttendee,
} from '@/db/sqlite';
import EmptyList from '@/components/EmptyList';
import checkedInDateFormat from '@/utils/checked-in-date-format';
import { useSQLiteContext } from 'expo-sqlite';
import RenderAttendeesStatus from '@/components/RenderAttendeesStatus';
import { Attendee, AttendeeStatus, TicketStatus } from '@/types/types';
import { manualCheckin } from '@/services/api';
import { useBuyers } from '@/hooks/useBuyers';
import eventBus from '@/eventBus';
import { useQueryClient } from '@tanstack/react-query';
const ITEM_HEIGHT = 100;
interface AllBuyersCard {
    attendee: any;
    errorMessage?: any;
    isLoading: any;
    handleTryAgain: (orderId: string, datetimeId: string) => void;
    handleManualCheckIn: (orderId: string, datetimeId: string) => void;
}

function AllBuyersCard({
    attendee,
    errorMessage,
    isLoading,
    handleTryAgain,
    handleManualCheckIn,
}: AllBuyersCard) {
    console.log("Rendered:", attendee.orderid);
    const [modalVisible, setModalVisible] = useState(false);

    const showConfirmation = () => {
        setModalVisible(true);
    };

    const handleConfirm = () => {
        setModalVisible(false);
        handleManualCheckIn(attendee.orderid, attendee.datetime_id);
    };

    const handleCancel = () => {
        setModalVisible(false);
    };
    return (
        <View
            // style={styles.list_org_wrp}
            style={{ height: ITEM_HEIGHT }}
            className={'flex-1 flex-row'}
            key={attendee.orderid}
        >
            <View className={'flex-1'}>
                <Text className={'text-lg font-bold'}>
                    {attendee.fname} {attendee.lname}
                </Text>
                <Text className={'text-md text-gray-700'}>
                    OrderId #{attendee?.orderid?.slice(-6)}
                </Text>
                <View>
                    <Text className={'text-md font-bold text-green-600'}>
                        Qty {attendee.total_qty}
                    </Text>
                </View>
            </View>

            <RenderAttendeesStatus
                attendee={attendee}
                errorMessage={errorMessage}
                handleTryAgain={handleTryAgain}
                isLoading={isLoading}
                showConfirmation={showConfirmation}
            />

            <Modal
                animationType="fade"
                transparent={true}
                visible={modalVisible}
                onRequestClose={handleCancel}
            >
                <View className="flex-1 justify-center items-center bg-black bg-opacity-50">
                    <View className="bg-white w-4/5 rounded-2xl p-6 shadow-xl">
                        <Text className="text-2xl font-bold text-center mb-2">
                            Confirm Check-In
                        </Text>
                        <View className="h-px bg-gray-200 w-full my-2" />
                        <Text className="text-xl text-center text-gray-700 mb-4">
                            Are you sure you want to check in {attendee.fname}{' '}
                            {attendee.lname} with Order #
                            {attendee?.orderid?.slice(-6)}?
                        </Text>
                        <View className="flex-row justify-between mt-2">
                            <TouchableOpacity
                                className="flex-1 bg-gray-200 p-3 rounded-md mr-2 items-center"
                                onPress={handleCancel}
                            >
                                <Text className="font-semibold text-gray-700">
                                    Cancel
                                </Text>
                            </TouchableOpacity>
                            <TouchableOpacity
                                className="flex-1 bg-green-500 p-3 rounded-md ml-2 items-center"
                                onPress={handleConfirm}
                            >
                                <Text className="font-semibold text-white">
                                    Yes
                                </Text>
                            </TouchableOpacity>
                        </View>
                    </View>
                </View>
            </Modal>
        </View>
    );
}

export default function AllBuyersTab() {
    const params = useLocalSearchParams();
    const db = useSQLiteContext(); /* Database reference */
    const queryClient = useQueryClient();

    const [isLoading, setIsLoading] = React.useState({});
    /* state to fix overlapping issue in rendering attendee list */
    const [initialRender, setInitialRender] = React.useState(true);
    /*  end of overlapping state */
    const [errorMessage, setErrorMessage] = React.useState({});
    const {
        eventData,
        eventLabel,
        datetimeId,
        refetch,
        searchQuery,
        isOnline,
    } = useDataProvider();
    const {
        buyers,
        loading,
        hasMore,
        loadMore,
        refetch: localRefetch,
    } = useBuyers(db, eventLabel as string, datetimeId as string, searchQuery);

    useEffect(() => {
        const listener = () => {
            refetch();
            localRefetch();
        };

        eventBus.on('ticketCheckedIn', listener);
        eventBus.on('reset', listener);

        return () => {
            eventBus.off('ticketCheckedIn', listener);
            eventBus.off('reset', listener);
        };
    }, []);

    const handleManualCheckIn = async (orderId: string, datetimeId: string) => {
        try {
            setIsLoading((prev) => ({ ...prev, [orderId]: true }));
            setErrorMessage((prev) => ({ ...prev, [orderId]: null }));

            // Check if ticket exists locally
            const existingTicket = await getAttendeesTicketStatus(
                db,
                eventData.events.event_id,
                orderId,
                datetimeId
            );

            const timestamp = await getLastSyncAt(
                db,
                eventLabel as string,
                datetimeId,
                1
            );

            const eventId = eventData.events.event_id;

            if (isOnline) {
                if (existingTicket) {
                    const checkinResult = await manualCheckin({
                        datetimeId,
                        orderId,
                        eventId,
                        lastSyncTime: timestamp?.lastSyncAt as string,
                    });
                    // if refunded_on has date that means we need to save checked_date null
                    // sync will be true that is 1
                    /*
                     * checked Date
                     * if there is checked_date and it status chk_status not already "1"
                     * */

                    // const eventId = eventData.events.event_id;
                    const basePayload = {
                        eventId: eventId,
                        orderId,
                        datetimeId,
                        sync: 1,
                        checkedInDate: checkinResult?.checked_date,
                        refunded_on: checkinResult?.refunded_on,
                        canceled_on: checkinResult?.canceled_on,
                        message: checkinResult?.message,
                    };

                    if (
                        checkinResult.att_detail.status ===
                        AttendeeStatus.CheckedIn
                    ) {
                        const payload = {
                            ...basePayload,
                            chk_status: TicketStatus.CHECKED,
                        };
                        await updateCancelledAttendeeTicketStatus(db, payload);
                        /*
                         * Note: In case of refunded and canceled users, no need to increment the order checked
                         * */
                        // Increment checked
                        await incrementCheckedCount(db, eventId, datetimeId);
                    } else if (
                        checkinResult.att_detail.status ===
                        AttendeeStatus.Refunded
                    ) {
                        const payload = {
                            ...basePayload,
                            chk_status: TicketStatus.UNCHECKED,
                        };
                        await updateCancelledAttendeeTicketStatus(db, payload);
                    } else if (
                        checkinResult.att_detail.status ===
                        AttendeeStatus.Canceled
                    ) {
                        const payload = {
                            ...basePayload,
                            chk_status: TicketStatus.UNCHECKED,
                        };
                        await updateCancelledAttendeeTicketStatus(db, payload);
                    }
                    // Emit Event so that we can always have TicketCheckedInTab refresh
                    eventBus.emit('ticketCheckedIn');
                    refetch();
                    localRefetch();
                    router.replace({
                        pathname: '/(tabs)/checked-in',
                        params: {
                            data: JSON.stringify(checkinResult),
                            // Below parameters used to redirect the user back on the page form where they came from
                            datetimeId: params.datetime_id,
                            dateId: params.date_id,
                            eventLabel: params.event_label,
                        },
                    });
                } else {
                    // if not ticket not exist locally but online
                    const checkinResult = await manualCheckin({
                        datetimeId,
                        orderId,
                        eventId,
                        lastSyncTime: timestamp?.lastSyncAt as string,
                    });
                    const ALREADY_CHECKED_MESSAGE =
                        CHECKIN_MESSAGES.ALREADY_CHECKED_IN_MESSAGE;
                    /* Since we don't have any other way to ensure that the below block run only once
                     * we are comparing the message, since we need to increment the checked count only once, not every time.
                     * */
                    if (
                        checkinResult.checked_date &&
                        checkinResult.message !== ALREADY_CHECKED_MESSAGE
                    ) {
                        /*
                         * Note: Do i need to use transaction here think about it.
                         * */
                        await incrementCheckedCount(db, eventId, datetimeId);
                        const { ticket_quantity: total_qty, ...rest } =
                            checkinResult.att_detail;

                        const attendee: Attendee = {
                            ...rest,
                            total_qty,
                            refund_status: rest.refund_status ?? null,
                        };
                        await insertAttendee(
                            db,
                            attendee,
                            checkinResult.events
                        );
                    }

                    eventBus.emit('ticketCheckedIn');
                    refetch();
                    router.replace({
                        pathname: '/(tabs)/checked-in',
                        params: {
                            data: JSON.stringify(checkinResult),
                            // Below parameters used to redirect the user back on the page form where they came from
                            datetimeId: params.datetime_id,
                            dateId: params.date_id,
                            eventLabel: params.event_label,
                        },
                    });
                }
            } else {
                if (!existingTicket) {
                    throw new Error('Ticket not found locally.');
                }
                let localDbResponse = null;
                const eventId = eventData.events.event_id;
                const basePayload = {
                    eventId: eventId,
                    orderId,
                    datetimeId,
                    sync: 0,
                };

                if (existingTicket.status === AttendeeStatus.CheckedIn) {
                    const payload = {
                        ...basePayload,
                        checkedInDate: checkedInDateFormat(),
                        chk_status: TicketStatus.CHECKED,
                    };
                    localDbResponse = await updateCancelledAttendeeTicketStatus(
                        db,
                        payload
                    );
                    /*
                     * Note: In case of refunded and canceled users, no need to increment the order checked
                     * */
                    // Increment checked
                    await incrementCheckedCount(db, eventId, datetimeId);
                } else if (existingTicket.status === AttendeeStatus.Refunded) {
                    /* Refund case */
                    const refundMessage = CHECKIN_MESSAGES.REFUND_MESSAGE;

                    const payload = {
                        ...basePayload,
                        checkedInDate: null,
                        refunded_on: existingTicket?.refunded_on,
                        canceled_on: null,
                        chk_status: TicketStatus.UNCHECKED,
                        message: refundMessage,
                    };
                    localDbResponse = await updateCancelledAttendeeTicketStatus(
                        db,
                        payload
                    );
                } else if (existingTicket.status === AttendeeStatus.Canceled) {
                    /*
                     * canceled
                     * */
                    const cancelledMessage = CHECKIN_MESSAGES.CANCELLED_MESSAGE;
                    const payload = {
                        ...basePayload,
                        checkedInDate: null,
                        refunded_on: null,
                        canceled_on: existingTicket?.canceled_on,
                        chk_status: TicketStatus.UNCHECKED,
                        message: cancelledMessage,
                    };
                    localDbResponse = await updateCancelledAttendeeTicketStatus(
                        db,
                        payload
                    );
                }
                /*
                 * checked Date
                 * if there is checked_date and it status chk_status not already "1"
                 * */
                eventBus.emit('ticketCheckedIn');
                refetch();
                localRefetch();
                router.replace({
                    pathname: '/(tabs)/checked-in',
                    params: {
                        data: JSON.stringify(localDbResponse),
                        // Below parameters used to redirect the user back on the page form where they came from
                        datetimeId: params.datetime_id,
                        dateId: params.date_id,
                        eventLabel: params.event_label,
                    },
                });
            }
        } catch (error: any) {
            console.log(error.message);
            const DELETE_MESSAGE = EVENT_ERROR_MESSAGES.DELETE_MESSAGE;
            const REJECTED_MESSAGE = EVENT_ERROR_MESSAGES.REJECTED_MESSAGE;
            const EXPIRED_MESSAGE = EVENT_ERROR_MESSAGES.EXPIRED_MESSAGE;

            if (error?.message?.includes(DELETE_MESSAGE)) {
                await cleanLocalDB(
                    db,
                    eventLabel as string,
                    datetimeId as string
                );
                await queryClient.invalidateQueries({ queryKey: ['evList'] });
                await queryClient.invalidateQueries({
                    queryKey: ['eventListProfile'],
                });
                alert(ERROR_MESSAGE_OVERRIDE.EVENT_MESSAGE);
                router.replace('/');
            } else if (error?.message?.includes(REJECTED_MESSAGE)) {
                await cleanLocalDB(
                    db,
                    eventLabel as string,
                    datetimeId as string
                );
                await queryClient.invalidateQueries({ queryKey: ['evList'] });
                await queryClient.invalidateQueries({
                    queryKey: ['eventListProfile'],
                });
                await queryClient.invalidateQueries({ queryKey: ['eventFV'] });
                alert(REJECTED_MESSAGE);
                router.replace('/');
            } else if (error?.message?.includes(EXPIRED_MESSAGE)) {
                await cleanLocalDB(
                    db,
                    eventLabel as string,
                    datetimeId as string
                );
                await queryClient.invalidateQueries({ queryKey: ['evList'] });
                await queryClient.invalidateQueries({ queryKey: ['eventFV'] });
                await queryClient.invalidateQueries({
                    queryKey: ['eventListProfile'],
                });
                alert(EXPIRED_MESSAGE);
                router.replace('/');
            }
            setErrorMessage((prev) => ({ ...prev, [orderId]: error.message }));
        } finally {
            setIsLoading((prev) => ({ ...prev, [orderId]: false }));
        }
    };

    const handleTryAgain = async (orderId: string, datetimeId: string) => {
        await handleManualCheckIn(orderId, datetimeId);
    };

    return (
        <FlatList
            data={buyers}
            ListEmptyComponent={
                <EmptyList
                    message={'No Buyers found'}
                    searchQuery={searchQuery}
                />
            }
            keyExtractor={(item) => item?.orderid?.toString()}
            renderItem={({ item }) => (
                <AllBuyersCard
                    key={item.orderid}
                    attendee={item}
                    errorMessage={errorMessage}
                    isLoading={isLoading}
                    handleTryAgain={handleTryAgain}
                    handleManualCheckIn={handleManualCheckIn}
                />
            )}
            showsVerticalScrollIndicator={false}
            onEndReached={loadMore}
            onEndReachedThreshold={1}
            initialNumToRender={10}
            maxToRenderPerBatch={30}
            getItemLayout={(data, index) => ({
                length: ITEM_HEIGHT, // height of each item
                offset: ITEM_HEIGHT * index,
                index,
            })}
            windowSize={10}
            removeClippedSubviews
            ListFooterComponent={
                loading ? <ActivityIndicator size="small" /> : null
            }
        />
    );
}

const styles = StyleSheet.create({
    list_org_wrp_lastrow: {
        paddingBottom: 70,
    },
    list_org_wrp: {
        // height: ITEM_HEIGHT,
        backgroundColor: '#fff',
        marginTop: 15,
        padding: 11,
        boxShadow: '0 7 10 rgba(0, 0, 0, 0.03)',
        borderRadius: 5,
        borderWidth: 1,
        borderColor: 'rgba(0, 0, 0, 0.125)',
    },
    nme_byr: {
        fontSize: 16,
        fontWeight: 600,
        marginBottom: 4,
    },
    list_orderbyr: {
        fontSize: 15,
        fontWeight: 400,
        marginBottom: 2,
        color: ' #808080',
    },
    list_qtybyr: {
        fontSize: 15,
        fontWeight: 500,
        marginBottom: 2,
        color: '#4ba03e',
    },
    btn_checkin_btn_trng: {
        height: '100%',
        alignItems: 'center',
        justifyContent: 'center',
        marginRight: 4,
    },
    checkinbtnmn: {
        padding: 10,
        borderRadius: 3,
    },
    chkedin_btntxtss: {
        fontWeight: 600,
    },
});

import React, { useEffect } from 'react';
import {
    View,
    Text,
    ScrollView,
    StyleSheet,
    ActivityIndicator,
} from 'react-native';
import { useDataProvider } from '@/provider/DataProvider';
import { FlatList } from 'react-native';
import { useSQLiteContext } from 'expo-sqlite';
import { useCheckedIn } from '@/hooks/useCheckedIn';
import EmptyList from '@/components/EmptyList';
import { useFocusEffect } from 'expo-router';
import eventBus from '@/eventBus';
import removeSecondsFromDateString from '@/utils/removeSecondsFromDateString';

const ITEM_HEIGHT = 100;
interface Attendee {
    orderid: string;
    fname: string;
    lname: string;
    total_qty: string;
    chk_status: string;
    checked_date: string;
}

function TicketCheckedInCard({ attendee }: { attendee: Attendee }) {
    console.log("Rendered:", attendee.orderid);

    return (
        <View
            // style={styles.list_org_wrp}
            style={{ height: ITEM_HEIGHT }}
            className={'flex-1 flex-row'}
            key={attendee?.orderid}
        >
            <View className={'flex-1'}>
                <Text style={styles.nme_byr}>
                    {attendee.fname} {attendee.lname}
                </Text>
                <Text style={styles.list_orderbyr}>
                    OrderId #{attendee?.orderid?.slice(-6)}
                </Text>
                <View>
                    <Text style={styles.list_qtybyr}>
                        Qty {attendee.total_qty}
                    </Text>
                </View>
            </View>

            <View className={'flex-1 items-end justify-center pr-3'}>
                <View className={'flex-1 items-center justify-center'}>
                    <Text className={'text-sm font-bold text-red-500'}>
                        Checked
                    </Text>
                    {attendee.checked_date && (
                        <Text className={'text-sm font-bold'}>
                            {/*{attendee.checked_date}*/}
                            {removeSecondsFromDateString(attendee.checked_date)}
                        </Text>
                    )}
                </View>
            </View>
        </View>
    );
}

export default function TicketCheckedInTab() {
    const db = useSQLiteContext();
    const {
        eventData,
        eventLabel,
        datetimeId,
        refetch,
        searchQuery,
        setSearchQuery,
        isOnline,
    } = useDataProvider();
    const {
        buyers,
        loading,
        hasMore,
        loadMore,
        refetch: localRefetch,
    } = useCheckedIn(
        db,
        eventLabel as string,
        datetimeId as string,
        searchQuery
    );

    useFocusEffect(
        React.useCallback(() => {
            // refetch(); // reload buyers whenever tab is focused
            // // if(searchQuery) {
            //     setSearchQuery('') // we need to set the search here everytime we switch to checked-in tab, since the search is global if we don't do this then it will reflect the last searched value in checked-in tab
            // }
            localRefetch();
        }, [eventLabel, datetimeId])
    );

    useEffect(() => {
        const listener = () => {
            // refetch();
            localRefetch();
        };

        eventBus.on('ticketCheckedIn', listener);
        eventBus.on('reset',listener);

        return () => {
            eventBus.off('ticketCheckedIn', listener);
            eventBus.off('reset',listener);
        };
    }, []);
    /* state to fix overlapping issue in rendering attendee list */
    // const [initialRender, setInitialRender] = React.useState(true);
    /*  end of overlapping state */
    // const CheckedInConfirmationList = eventData?.attendees?.filter(
    //     (attendee: Attendee) => attendee?.chk_status.toString() === '1'
    // );
    return (
        <FlatList
            data={buyers}
            ListEmptyComponent={
                <EmptyList
                    message={'No Check-in Record found'}
                    searchQuery={searchQuery}
                />
            }
            keyExtractor={(item) => item?.orderid?.toString()}
            renderItem={({ item }) => <TicketCheckedInCard attendee={item} />}
            showsVerticalScrollIndicator={false}
            onEndReached={loadMore}
            onEndReachedThreshold={1}
            initialNumToRender={10}
            // initialNumToRender={buyers.length}
            getItemLayout={(data, index) => ({
                length: ITEM_HEIGHT,   // height of each item
                offset: ITEM_HEIGHT * index,
                index,
            })}
            maxToRenderPerBatch={30}
            windowSize={10}
            removeClippedSubviews={false}
            ListFooterComponent={
                loading ? <ActivityIndicator size="small" /> : null
            }
        />
        //  <ScrollView showsVerticalScrollIndicator={false}>
        //     {CheckedInConfirmationList?.map((item) => (
        //         <TicketCheckedInCard key={item?.orderid?.toString()} attendee={item} />
        //     ))}
        //
        //    {/* Intentionally left paddingBottom so that QRcode scanner will not overlay on event list */}
        //     <View style={styles.list_org_wrp_lastrow} className={'flex-1 flex-row'}></View>
        //
        // </ScrollView>
    );
}

const styles = StyleSheet.create({
    list_org_wrp_lastrow: {
        paddingBottom: 70,
    },
    list_org_wrp: {
        backgroundColor: '#fff',
        marginTop: 15,
        padding: 11,
        boxShadow: '0 7 10 rgba(0, 0, 0, 0.03)',
        borderRadius: 5,
        borderWidth: 1,
        borderColor: 'rgba(0, 0, 0, 0.125)',
    },
    nme_byr: {
        fontSize: 16,
        fontWeight: 600,
        marginBottom: 4,
    },
    list_orderbyr: {
        fontSize: 15,
        fontWeight: 400,
        marginBottom: 2,
        color: ' #808080',
    },
    list_qtybyr: {
        fontSize: 15,
        fontWeight: 500,
        marginBottom: 2,
        color: '#4ba03e',
    },
});

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.