React Native FlatList onEndReached异常:重复触发且跳页求助
React Native 无限滚动 onEndReached 触发异常问题
问题现象
开发React Native卡片列表组件实现无限滚动时,出现以下异常:
- 仅滚动到底部一次,
onEndReached事件却触发约10次 - API请求的page参数出现跳号(如2、4、5、6、8、10等)
问题根源与修复方案
1. 缺少请求锁机制,重复触发page递增
当前仅依赖RTK Query的isFetching判断是否发起请求,但该状态更新存在延迟,且handleEndReached的依赖项不完整,导致多次触发时isFetching仍为false,进而重复执行setPage。
修复:
- 添加独立的
isLoadingMore状态锁,发起请求时锁定,请求完成后解锁 - 完善
handleEndReached的依赖项,确保状态变化时回调能正确更新
2. RTK Query分页数据未合并
当前直接使用data作为列表数据源,但RTK Query默认不会自动合并分页返回的数据,每次新请求会覆盖旧数据,导致列表长度未增加,onEndReached持续触发。
修复:
- 手动合并分页数据到本地状态,或改用RTK Query的
useInfiniteQuery(专为分页场景设计,自动管理数据合并)
3. 自定义List组件的加载逻辑问题
handleLoading中依赖page参数,但Places组件未将该参数传入ListLayout,导致分页加载时底部加载指示器不显示,用户无法感知加载状态,可能继续滚动触发多次请求。
修复:
- 在Places组件中传递
page参数给ListLayout - 修正
handleLoading的条件判断与布局属性,确保分页加载时正确显示加载指示器
修改后的代码
Places组件
import { FlatList, StyleSheet, View } from 'react-native'; import React, { useCallback, useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useGetNearbyPlacesQuery } from '@redux/queries/generalAPI'; import StoreCardSkeleton from '@components/StoreCard/CardSkeleton'; import StoreCard from '@components/StoreCard'; import { hp } from '@utils/responsive'; import ListLayout from '@components/ListLayout'; import { selectSessionLocation } from '@redux/reducers/authSlice'; const LIMIT = 10; const Places = ({ route }) => { const category = route?.params?.category; const { t } = useTranslation(); const [page, setPage] = useState(1); const [isLoadingMore, setIsLoadingMore] = useState(false); const [allPlaces, setAllPlaces] = useState([]); const sessionLocation = useSelector(selectSessionLocation); const { data: newPlaces, isFetching, refetch, } = useGetNearbyPlacesQuery({ lat: sessionLocation?.[1], lng: sessionLocation?.[0], page, limit: LIMIT, type: category, }); // 合并新分页数据到本地列表 useEffect(() => { if (newPlaces && newPlaces.length > 0) { setAllPlaces(prev => [...prev, ...newPlaces]); setIsLoadingMore(false); } }, [newPlaces]); const renderItem = useCallback( ({ item }) => ( <View style={styles.item}> <StoreCard logo={item?.logo} location={item?.buildingName} typeOfSector={item?.typeOfSector} storeId={item._id} name={item?.vendorName} /> </View> ), [] ); const renderSkeletons = useCallback(() => { if (!isFetching) return null; const skeletonArray = Array.from({ length: 10 }); return skeletonArray.map((_, index) => ( <View style={styles.item} key={index}> <StoreCardSkeleton /> </View> )); }, [isFetching]); const handleRefresh = () => { setPage(1); setAllPlaces([]); refetch(); }; const handleEndReached = useCallback(() => { // 避免重复加载:检查加载状态、是否还有更多数据 if (!isLoadingMore && !isFetching && newPlaces?.length === LIMIT) { setIsLoadingMore(true); setPage(prev => prev + 1); } }, [isLoadingMore, isFetching, newPlaces]); return ( <ListLayout title={t('nearby_places')} onRefresh={handleRefresh} renderLoading={renderSkeletons} onEndReached={handleEndReached} onEndReachedThreshold={0.2} loading={isFetching || isLoadingMore} data={allPlaces} renderItem={renderItem} keyExtractor={(item) => item?._id} page={page} /> ); }; export default Places; const styles = StyleSheet.create({ item: { marginBottom: hp(20), }, });
自定义List组件
import { RefreshControl, StyleSheet, FlatList, ActivityIndicator, View, } from 'react-native'; import React from 'react'; import EmptyResult from '@components/EmptyResult'; import Colors from '@constants/Colors'; import { hp } from '@utils/responsive'; const List = ({ data = [], keyExtractor, renderItem = () => {}, listStyle, loading = false, renderLoading = () => {}, onEndReached = () => {}, onRefresh = () => {}, onEndReachedThreshold = 0.2, page, }) => { const renderEmptyList = () => { if (loading) return null; return <EmptyResult />; }; const defaultKeyExtractor = (item, index) => item._id + index.toString(); const handleLoading = () => { if (page === 1) { return renderLoading(); } else if (loading) { return ( <View style={styles.loadingWrapper}> <ActivityIndicator size="small" color={Colors.RED} /> </View> ); } return null; }; return ( <FlatList data={data} keyExtractor={keyExtractor || defaultKeyExtractor} renderItem={renderItem} showsVerticalScrollIndicator={false} contentContainerStyle={[styles.list, listStyle]} onEndReachedThreshold={onEndReachedThreshold} ListEmptyComponent={renderEmptyList} ListFooterComponent={handleLoading} onEndReached={onEndReached} extraData={[data, loading, page]} refreshControl={ <RefreshControl refreshing={loading} onRefresh={onRefresh} tintColor={Colors.RED} colors={[Colors.RED]} /> } /> ); }; export default React.memo(List); const styles = StyleSheet.create({ list: { flexGrow: 1, paddingBottom: hp(30), }, loadingWrapper: { alignItems: 'center', justifyContent: 'center', paddingVertical: hp(10), }, });
内容的提问来源于stack exchange,提问作者warCommander
相关产品推荐
相关产品推荐

