You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 21:44:49