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

React Native中如何实现FlatList数据渐进式加载?

实现React Native FlatList渐进式加载(分页渲染)

先修复现有代码核心bug

原useHttpClient中res.json()未加await,导致返回Promise而非解析后的数据,直接引发数据处理异常:

// 修改useHttpClient的sendRequest方法
const sendRequest = useCallback(
  async (URL, method = "GET", body = null, headers = {}, credentials = "include") => {
    setIsLoading(true);
    try {
      const res = await fetch(URL, { method, body, headers, credentials });
      const resData = await res.json(); // 必须添加await!
      
      if (!res.ok) {
        throw new Error(resData.error || '请求失败');
      }
      setIsLoading(false);
      return resData;
    } catch (error) {
      setIsLoading(false);
      setError(error.message);
      throw error; // 抛出错误让调用方捕获处理
    }
  },
  []
);

后端适配分页接口

需要后端支持分页参数(如page页码、limit每页条数),并返回分页元数据(如totalPages总页数),接口示例:
GET /api/location?lng=37.7749295&lat=-122.4194155&page=1&limit=10
返回结构示例:

{
  "results": [...],
  "totalPages": 5,
  "totalCount": 48
}

前端实现分页加载逻辑

修改RestaurantsScreen,添加分页状态管理和滚动触底加载:

import { useState, useEffect, useContext, memo } from "react";
import { SafeAreaView, Text, View, Button } from "react-native";
import { RestaurantsContext } from "../contexts/RestaurantsContext";
import { useHttpClient } from "../hooks/useHttpClient";
import RestaurantList from "../components/RestaurantList";
import Searchbar from "../components/Searchbar";
import Spacer from "../components/Spacer";
import Center from "../components/Center";

// 用memo优化渲染项,避免不必要重渲染
const renderItemComponent = memo(({ item }) => {
  return (
    <Spacer dir="vertical" size="small">
      <RestaurantCard restaurant={item} />
    </Spacer>
  );
});

export const RestaurantsScreen = () => {
  const { isLoading, error, clearError, sendRequest } = useHttpClient();
  const { restaurants, setRestaurants } = useContext(RestaurantsContext);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);
  const [isLoadingMore, setIsLoadingMore] = useState(false);

  // 加载指定页码的数据
  const fetchPage = async (currentPage) => {
    if (currentPage > 1) setIsLoadingMore(true);
    
    try {
      const URL = `http://localhost:3000/api/location?lng=37.7749295&lat=-122.4194155&page=${currentPage}&limit=10`;
      const { results, totalPages } = await sendRequest(URL);
      
      const mappedResult = results.map((restaurant) => ({
        ...restaurant,
        isOpenNow: restaurant.opening_hours?.open_now,
        isClosedTemporarily: restaurant.business_status === "CLOSED_TEMPORARILY",
      }));
      const loadedRestaurants = camelize(mappedResult);

      // 合并或替换现有数据
      if (currentPage === 1) {
        setRestaurants(loadedRestaurants);
      } else {
        setRestaurants(prev => [...prev, ...loadedRestaurants]);
      }

      // 判断是否还有更多数据
      setHasMore(currentPage < totalPages);
    } catch (err) {
      console.error('加载失败:', err);
    } finally {
      if (currentPage > 1) setIsLoadingMore(false);
    }
  };

  // 初始加载第一页
  useEffect(() => {
    fetchPage(1);
  }, []);

  // 页码变化时加载对应页
  useEffect(() => {
    if (page > 1) {
      fetchPage(page);
    }
  }, [page]);

  // 触底加载下一页
  const loadMore = () => {
    if (hasMore && !isLoadingMore && !isLoading) {
      setPage(prev => prev + 1);
    }
  };

  return (
    <SafeAreaView style={{flex: 1}}>
      <View style={{paddingHorizontal: 16, paddingVertical: 8}}>
        <Searchbar placeholder="Search ..." />
      </View>

      {/* 错误提示 */}
      {error && (
        <Center style={{padding: 16}}>
          <Text style={{color: '#ff3b30'}}>{error}</Text>
          <Button title="重试" onPress={() => fetchPage(page)} />
          <Button title="清除错误" onPress={clearError} />
        </Center>
      )}

      {/* 初始加载状态 */}
      {isLoading && restaurants.length === 0 && (
        <Center style={{flex: 1}}>
          <Text>Loading Restaurants ... </Text>
        </Center>
      )}

      {/* 空状态 */}
      {!isLoading && restaurants.length === 0 && !error && (
        <Center style={{flex: 1}}>
          <Text>No Restaurants Available</Text>
        </Center>
      )}

      {/* 列表渲染 */}
      {restaurants.length > 0 && (
        <RestaurantList
          data={restaurants}
          renderItem={renderItemComponent}
          keyExtractor={(item) => item.id || item.name} // 优先用唯一ID,避免名称重复
          initialNumToRender={10}
          onEndReached={loadMore}
          onEndReachedThreshold={0.1} // 滚动到距离底部10%时触发加载
          ListFooterComponent={isLoadingMore ? (
            <Center style={{paddingVertical: 16}}>
              <Text>加载更多...</Text>
            </Center>
          ) : null}
          // 若列表项高度固定,添加getItemLayout提升性能
          getItemLayout={(data, index) => ({
            length: 200, // 替换为你的RestaurantCard实际高度
            offset: 200 * index,
            index
          })}
        />
      )}
    </SafeAreaView>
  );
};

关键优化说明

  • 分页状态管控:通过page、hasMore、isLoadingMore避免重复请求,控制加载流程
  • 状态更新规范:使用context提供的setRestaurants方法更新数据,确保组件触发重渲染
  • 性能优化:用React.memo包裹渲染项、添加getItemLayout减少FlatList重计算,提升滚动流畅度
  • 错误处理:完善捕获和重试逻辑,避免网络问题导致应用崩溃
  • 用户体验:区分初始加载和加载更多的状态反馈,避免用户困惑

内容的提问来源于stack exchange,提问作者Mahdyar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 21:57:10