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

React Native FlatList重复渲染及useMemo Hook报错问题求助

问题解决:FlatList重复渲染与Hook错误修复

核心问题分析

  1. renderItem用法错误:FlatList的renderItem会自动遍历data数组,每次调用只负责渲染单个项。你现在在renderItem里手动遍历result数组,导致FlatList的每一项都渲染整个列表,直接造成重复项。
  2. Hook调用顺序违规:你把useMemo放在了if (loading)条件返回之后,违反了React Hooks必须在组件顶层调用的规则,导致渲染时Hook数量不一致,触发rendered more hooks than previous render错误。
  3. 缓存方式错误:缓存renderItem应该用useCallback(缓存函数)而非useMemo(缓存值)。

修复步骤与代码示例

1. 修正renderItem函数

让renderItem接收FlatList传入的{item, index}参数,只渲染单个条目,并用useCallback缓存函数:

const renderItem = useCallback(({ item, index }) => {
  console.log("renderItem called for item:", item);
  return (
    <TouchableOpacity
      onPress={() =>
        navigation.navigate("viewRecipeScreen", {
          recipe: item,
        })
      }
      key={item.id || index} // 优先用item的唯一ID,无ID再用index
    >
      <Image
        source={{
          uri:
            item?.imagepath === "empty"
              ? "https://static.vecteezy.com/system/resources/previews/005/337/799/original/icon-image-not-found-free-vector.jpg"
              : item?.imagepath,
        }}
        style={styles.recipeImage}
      />
    </TouchableOpacity>
  );
}, [navigation]);

2. 调整Hook位置

把renderItem的定义移到组件顶层(if (loading)之前),确保Hook调用顺序稳定,同时删除无用的memorenderItem变量。

3. 优化FlatList配置

确保data直接传入result,renderItem使用修正后的函数:

<FlatList
  data={result}
  renderItem={renderItem}
  keyExtractor={(item) => item.id || item.imagepath} // 用更稳定的key
  horizontal={true}
  windowSize={1}
/>

完整修正代码

import React, {
  useEffect,
  useContext,
  useState,
  useCallback,
} from "react";
import {
  View,
  Text,
  StyleSheet,
  TextInput,
  Image,
  FlatList,
  TouchableOpacity,
} from "react-native";
import Button from "react-native-button";
import { auth } from "../firebase/firebase";
import { onAuthStateChanged } from "firebase/auth";
import { doc, getDoc } from "firebase/firestore";
import { db } from "../firebase/firebase";
import { AuthContext } from "../navigation/AuthProvider";

export function UserHome({ navigation }) {
  const { user, setName, setUserName, userName, setBioText, logout } =
    useContext(AuthContext);
  const [queryText, setQueryText] = useState("");
  const [result, setResult] = useState([]);
  const [loading, setLoading] = useState(true);

  const [searchPressed, setSearchPressed] = useState(false);
  const [likes, setLikes] = useState("");
  const [dislikes, setDislikes] = useState("");
  const [survey, setSurvey] = useState(null);
  const [searchResults, setSearchResults] = useState([]); // 补充缺失的state

  // 修正:用useCallback缓存renderItem,放在组件顶层
  const renderItem = useCallback(({ item, index }) => {
    console.log("renderItem called");
    return (
      <TouchableOpacity
        onPress={() =>
          navigation.navigate("viewRecipeScreen", {
            recipe: item,
          })
        }
        key={item.id || index}
      >
        <Image
          source={{
            uri:
              item?.imagepath === "empty"
                ? "https://static.vecteezy.com/system/resources/previews/005/337/799/original/icon-image-not-found-free-vector.jpg"
                : item?.imagepath,
          }}
          style={styles.recipeImage}
        />
      </TouchableOpacity>
    );
  }, [navigation]);

  const handleSignOut = () => {
    logout();
  };

  const handleInputChange = (text) => {
    setQueryText(text);
  };

  useEffect(() => {
    const getName = async (userId) => {
      const docRef = doc(db, "userInfo", userId);
      const docSnap = await getDoc(docRef);
      const full_name = docSnap.get("fullname");
      const user_name = docSnap.get("username");
      const bio_Text = docSnap.get("bio");
      const likes = docSnap.get("faveRecs");
      setLikes(likes);
      const dislikes = docSnap.get("badRecs");
      const surveyResults = docSnap.get("tolerance_questionnaire");
      setSurvey(surveyResults);
      setDislikes(dislikes);
      if (docSnap.exists()) {
        setName(full_name);
        setUserName(user_name);
        setBioText(bio_Text);
      } else {
        console.log("No username, bio, or full name found");
      }
    };
    onAuthStateChanged(auth, (user) => {
      if (user) {
        getName(user.uid);
      }
    });
  }, []);

  useEffect(() => {
    const fetchRecipes = async () => {
      try {
        const palette_body = {
          body: {
            recipeCategory: null,
            user_info: { likes, dislikes, survey },
          },
        };

        const response = await fetch(
          `https://25jg62s1m6.execute-api.us-east-1.amazonaws.com/test/kohoquery/palette_analyzer`,
          {
            method: "POST",
            body: JSON.stringify(palette_body),
            headers: {
              "Content-Type": "application/json",
            },
          }
        );
        const result_data = await response.json();

        setResult(JSON.parse(result_data.body));
        setLoading(false);
      } catch (error) {
        console.log("Error parsing JSON data!!!:", error);
        setLoading(false);
      }

      try {
        if (queryText) {
          const response = await fetch(
            `https://25jg62s1m6.execute-api.us-east-1.amazonaws.com/test/kohoquery?queryText=${queryText}`
          );
          const data = await response.json();
          setSearchResults(data.slice(0, 4));

          navigation.navigate("recipeDetailsScreen", {
            recipe: data.slice(0, 4),
          });
        }
      } catch (error) {
        console.error(error);
      }
    };
    fetchRecipes();
  }, [searchPressed, queryText, likes, dislikes, survey, navigation]);

  if (loading) {
    return <Text>Loading...</Text>;
  }

  return (
    <View style={styles.container}>
      <View style={styles.headerContainer}>
        <Image
          source={require("../assets/kohomainlogo.png")}
          style={styles.logo}
        />
        <View style={styles.welcomeContainer}>
          <Text style={styles.welcomeText}>Signed in as: @{userName}!</Text>
          <Text style={styles.cookText}>What would you like to cook?</Text>
        </View>
        <TextInput
          style={styles.input}
          onChangeText={handleInputChange}
          value={queryText}
          placeholder="Enter your dish name"
          placeholderTextColor="grey"
          returnKeyType="search"
          onSubmitEditing={() => setSearchPressed(!searchPressed)}
        />
      </View>

      <View style={styles.recipeContainer}>
        <Text style={styles.recipeTitle}>We think you'd like these!</Text>
        <FlatList
          data={result}
          renderItem={renderItem}
          keyExtractor={(item) => item.id || item.imagepath}
          horizontal={true}
          windowSize={1}
        />
      </View>

      <View style={styles.recipeContainerPopular}>
        <Text style={styles.recipeTitle}>Most Popular</Text>
      </View>

      <View style={styles.bottomContainer}>
        <Button
          containerStyle={{
            padding: 10,
            height: 45,
            overflow: "hidden",
            borderRadius: 20,
            backgroundColor: "#8252ff",
          }}
          style={{
            fontSize: 18,
            color: "white",
            textAlign: "center",
          }}
          onPress={handleSignOut}
        >
          Sign Out
        </Button>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
  },
  headerContainer: {
    backgroundColor: "#fff",
    height: 80,
    justifyContent: "center",
    alignItems: "center",
    borderBottomWidth: 1,
    borderBottomColor: "#ccc",
    width: "100%",
  },
  logo: {
    width: 150,
    height: 75,
    resizeMode: "contain",
    marginTop: 150,
  },
  bottomContainer: {
    flex: 1,
    justifyContent: "flex-end",
    marginBottom: 20,
  },
  welcomeContainer: {
    paddingLeft: 10,
  },
  welcomeText: {
    fontSize: 18,
  },
  cookText: {
    fontSize: 18,
    fontWeight: "bold",
  },
  input: {
    height: 40,
    width: "80%",
    borderColor: "gray",
    borderWidth: 1,
    marginBottom: 20,
    padding: 10,
    borderRadius: 10,
    marginTop: 10,
    backgroundColor: "white",
  },
  recipeContainer: {
    flexDirection: "column",
    justifyContent: "space-around",
    alignItems: "center",
    marginTop: 150,
    width: "100%",
  },
  recipeContainerPopular: {
    flexDirection: "column",
    justifyContent: "space-around",
    alignItems: "center",
    marginTop: 20,
    width: "100%",
  },
  recipeImage: {
    width: 400,
    height: 200,
    resizeMode: "cover",
    borderRadius: 10,
    marginHorizontal: 5,
  },
  recipeTitle: {
    fontSize: 24,
    fontWeight: "bold",
    marginBottom: 10,
    marginTop: 10,
    textAlign: "left",
    width: "90%",
  },
});

额外说明

  • 补充了原代码中缺失的searchResults state,避免未定义错误。
  • 优化了keyExtractor,使用更稳定的标识替代index,减少列表更新时的渲染异常。
  • 调整了recipeTitle的样式,确保标题左对齐生效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 22:07:03