React Native无限滚动触发Too many re-renders错误排查求助
React Native无限滚动「Too many re-renders」错误修复
问题描述
开发React Native无限滚动功能时触发错误:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
错误定位在ReceivedBoxes.jsx的getReceivedBoxes函数(53:33),推测由间接修改状态导致,但无法准确定位问题。
错误原因分析
onEndReached触发循环:FlatList的onEndReached直接传入getReceivedsBoxes函数,该函数内部会修改currentPage、loading等状态。每次状态更新触发组件重渲染,重渲染后FlatList会再次触发onEndReached;加上初始lastPage为undefined,currentPage > lastPage的判断不生效,导致无限调用getReceivedsBoxes。- 函数未缓存:
getReceivedsBoxes未用useCallback包裹,组件每次重渲染都会创建新的函数实例,FlatList的onEndReached属性频繁变化,加剧重复触发。 - 初始加载逻辑:两个
useEffect分别调用数据获取函数,但未处理依赖项,可能导致不必要的重复执行。
修复步骤
- 用
useCallback缓存getReceivedsBoxes,确保函数仅在依赖项变化时重新创建,避免FlatList重复触发onEndReached。 - 修正分页判断逻辑,处理
lastPage为undefined的初始状态,避免无效请求。 - 调整
useEffect依赖项,确保初始加载只执行一次,或在token变化时重新加载数据。 - 优化
FlatList的onEndReached调用,确保仅在滚动到底部且非加载状态时触发请求。
修复后的完整代码
import { useCallback, useContext, useEffect, useState } from "react"; import { FlatList, ScrollView, TouchableOpacity, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { Box, Center, Text } from "native-base"; import { useNavigation } from "@react-navigation/native"; import { authContext } from "../contexts/auth.context"; import { Api } from "../utils/api.util"; import { HeaderComponent } from "../components/Header"; import { ButtonComponent } from "../components/Button"; import { MemoizedTouchableReceivedBox } from "../components/TouchableReceivedBox"; import { MemoizedTouchableBoxReceivedInProcessing } from "../components/TouchableReceivedBoxInProcessing"; export function ReceivedBoxes() { const navigation = useNavigation(); const { token } = useContext(authContext); const [receivedBoxes, setReceivedBoxes] = useState([]); const [rbInProcessing, setRBInProcessing] = useState([]); const [loading, setLoading] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [lastPage, setLastPage] = useState(undefined); // 初始加载处理中数据 useEffect(() => { getReceivedBoxesInProcessing(); }, [token]); // 初始加载已接收数据 useEffect(() => { if (token) { getReceivedsBoxes(); } }, [token]); // 用useCallback缓存函数,避免重复创建 const getReceivedsBoxes = useCallback(async () => { // 加载中或已到最后一页则返回 if (loading || (lastPage !== undefined && currentPage > lastPage)) return; setLoading(true); try { const response = await Api( { route: `receiving?page=${currentPage}&image_size=small`, method: "GET", }, token ); const { success, receiving, pagination } = await response.json(); if (success) { setReceivedBoxes((prev) => [...prev, ...receiving]); setLastPage(pagination.last_page); setCurrentPage(pagination.current_page + 1); } } catch (e) { console.warn(e); } finally { setLoading(false); } }, [loading, currentPage, lastPage, token]); async function getReceivedBoxesInProcessing() { try { const response = await Api( { route: `receiving/processing?page=1&image_size=small`, method: "GET", }, token ); const { success, receiving } = await response.json(); if (success) { setRBInProcessing(receiving); } } catch (e) { console.warn(e); } } const renderItemReceivedBox = useCallback(({ item }) => ( <MemoizedTouchableReceivedBox key={item.id} // 用item.id作为key更稳定,避免index变化导致的重渲染 id={item.id} title={item.origin_tracking_number} qty={item.qty} weight_pound={item.weight_lbs} images={item.images} date={String(item.received_date)} /> ), []); function EmptyListMessage() { return ( <View style={{ alignItems: "center", justifyContent: "center" }}> <Text textAlign="center">Não há caixas recebidas a serem exibidas</Text> </View> ); } return ( <SafeAreaView style={{ backgroundColor: "#e6e6e6", height: "100%" }}> <HeaderComponent hasArrowBackButton={true} title="Caixas Recebidas" /> <View style={{ marginHorizontal: 10, marginTop: 5 }}> <Center> <Box width="80%"> <ButtonComponent buttonProps={{ text: "Localize sua encomenda", isBold: true, action: () => { navigation.navigate("LocateUrOrder"); }, }} /> </Box> </Center> <Box marginBottom={6}> <Box marginTop={6} marginBottom={3} width="100%" flexDirection="row" justifyContent="space-between" > <Box> <Text fontSize={18} color="#15294B"> Caixas em processamento </Text> </Box> <TouchableOpacity onPress={() => {}}> <Box backgroundColor="#FFF" paddingX={5} paddingY={1} borderRadius={15}> <Text fontSize={16} color="#894AFF"> Ver tudo </Text> </Box> </TouchableOpacity> </Box> <ScrollView horizontal> {rbInProcessing.map((receivedBox) => ( <MemoizedTouchableBoxReceivedInProcessing key={receivedBox.id} id={receivedBox.id} title={receivedBox.origin_tracking_number} images={receivedBox.images} date={receivedBox.received_date} /> ))} </ScrollView> </Box> <Box marginBottom={6}> <View style={{ height: "90%" }}> <FlatList data={receivedBoxes} renderItem={renderItemReceivedBox} keyExtractor={(item) => item.id.toString()} // 用item.id作为key更稳定 onEndReachedThreshold={0.1} onEndReached={() => { if (!loading) getReceivedsBoxes(); }} ListEmptyComponent={EmptyListMessage} contentContainerStyle={{ paddingBottom: 100 }} /> </View> </Box> </View> </SafeAreaView> ); }
额外优化点
- 将
keyExtractor的key从index改为item.id,避免列表项顺序变化导致的不必要重渲染。 - 移除了冗余的
useMemo包裹renderItemReceivedBox,因为useCallback已经缓存了该函数。 - 在
onEndReached中增加了loading判断,确保同一时间只有一个请求在进行。
内容的提问来源于stack exchange,提问作者Alisson
相关产品推荐
相关产品推荐

