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

React Native FlatList数据变更不更新/重渲染问题排查求助

React Native FlatList 功能问题排查

问题描述

  • 作为FlatList data属性的stream状态变化时,FlatList并非总是重渲染/更新;
  • 通过滑动操作进行归档/取消归档时,FlatList不会立即移除对应条目,需手动切换视图才能看到变化;
  • 过滤功能无法直接应用到状态,需点击两次才能生效。

相关代码

//React
import { View, StyleSheet, Pressable, Animated, FlatList } from "react-native";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";

//Internal
import SelectBtn from "./SelectBtn";
import SessionComponent from "./SessionComponent";
import LoadingOverlay from "../notification/LoadingOverlay";
import { SessionsContext } from "../../store/context-reducer/sessionsContext";

//External
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { database, auth } from "../../firebase";
import { ref, onValue, remove, update } from "firebase/database";

function SessionStream() {
  const navigation = useNavigation();

  const sessionsCtx = useContext(SessionsContext);
  const currentSessions = sessionsCtx.sessions;

  const [isFetching, setIsFetching] = useState(true);

  const [stream, setStream] = useState([]);
  const [inbox, setInbox] = useState([]);
  const [archive, setArchive] = useState([]);
  const [filter, setFilter] = useState([]);

  const sessionList = ["Sessions", "Archive"];
  const sortList = ["ABC", "CBA", "Latest Date", "Earliest Date"];

  useEffect(() => {
    //Fetches all sessions from the database
    async function getSessions() {
      setIsFetching(true);
      const uid = auth.currentUser.uid;
      const sessionsRef = ref(database, "users/" + uid + "/sessions/");
      try {
        onValue(sessionsRef, async (snapshot) => {
          const response = await snapshot.val();
          if (response !== null) {
            const responseObj = Object.entries(response);
            const sessionsData = responseObj.map((item) => {
              return {
                id: item[1].id,
                title: item[1].title,
                laps: item[1].laps,
                endTime: item[1].endTime,
                note: item[1].note,
                identifier: item[1].identifier,
                date: item[1].date,
                smed: item[1].smed,
                externalRatio: item[1].externalRatio,
                internalRatio: item[1].internalRatio,
                untrackedRatio: item[1].untrackedRatio,
                archived: item[1].archived,
              };
            });
            sessionsCtx.setSession(sessionsData);
            setIsFetching(false);
          } else {
            sessionsCtx.setSession([]);
            setIsFetching(false);
          }
        });
      } catch (err) {
        alert(err.message);
        setIsFetching(false);
      }
    }
    getSessions();
  }, []);

  useEffect(() => {
    //Sorts sessions into archived and unarchived
    setInbox(
      currentSessions.filter((session) => {
        return session.archived === false || session.archived === undefined;
      })
    );
    setArchive(
      currentSessions.filter((session) => {
        return session.archived === true;
      })
    );
  }, [currentSessions, archiveHandler, unArchiveHandler, sessionsCtx, stream]);

  if (isFetching) {
    setTimeout(() => {
      return <LoadingOverlay />;
    }, 5000);
  }

  const onPressHandler = useCallback(
    //Callback to open the session
    (item) => {
      navigation.navigate("Detail", {
        sessionID: item.id,
      });
    },
    [onPressHandler]
  );

  const rightSwipeActions = useCallback(
    //Swipe actions for the session list
    (item, swipeAnimatedValue) => {
      return (
        <View
          style={{
            flexDirection: "row",
            width: 168,
            height: 132,
          }}
        >
          {item.archived === false ? (
            <Pressable
              onPress={archiveHandler.bind(this, item)}
              style={({ pressed }) => pressed && styles.swipePressed}
            >
              <View style={styles.archive}>
                <Animated.View
                  style={[
                    styles.archive,
                    {
                      transform: [
                        {
                          scale: swipeAnimatedValue.interpolate({
                            inputRange: [0, 1],
                            outputRange: [0, 1],
                            extrapolate: "clamp",
                          }),
                        },
                      ],
                    },
                  ]}
                >
                  <Ionicons
                    name="ios-archive-outline"
                    size={24}
                    color="white"
                  />
                </Animated.View>
              </View>
            </Pressable>
          ) : (
            <Pressable
              onPress={unArchiveHandler.bind(this, item)}
              style={({ pressed }) => pressed && styles.swipePressed}
            >
              <View style={styles.unArchive}>
                <Animated.View
                  style={[
                    styles.unArchive,
                    {
                      transform: [
                        {
                          scale: swipeAnimatedValue.interpolate({
                            inputRange: [0, 1],
                            outputRange: [0, 1],
                            extrapolate: "clamp",
                          }),
                        },
                      ],
                    },
                  ]}
                >
                  <Ionicons
                    name="md-duplicate-outline"
                    size={24}
                    color="white"
                  />
                </Animated.View>
              </View>
            </Pressable>
          )}

          <Pressable
            onPress={deleteHandler.bind(this, item)}
            style={({ pressed }) => pressed && styles.pressed}
          >
            <View style={styles.trash}>
              <Animated.View
                style={[
                  styles.trash,
                  {
                    transform: [
                      {
                        scale: swipeAnimatedValue.interpolate({
                          inputRange: [0, 1],
                          outputRange: [0, 1],
                          extrapolate: "clamp",
                        }),
                      },
                    ],
                  },
                ]}
              >
                <Ionicons name="trash-outline" size={24} color="white" />
              </Animated.View>
            </View>
          </Pressable>
        </View>
      );
    },
    [rightSwipeActions]
  );

  const deleteHandler = useCallback(
    (item) => {
      try {
        sessionsCtx.deleteSession(item.id); // delete from local context
        const uid = auth.currentUser.uid;
        const sessionRef = ref(
          database,
          "users/" + uid + "/sessions/" + item.id
        );
        remove(sessionRef); // delete from firebase
      } catch (error) {
        alert(error.message);
      }
    },
    [deleteHandler]
  );

  const archiveHandler = (item) => {
    try {
      const id = item.id;
      const updatedSession = {
        ...item, // copy current session
        archived: true,
      };
      const uid = auth.currentUser.uid;
      const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
      update(sessionRef, updatedSession);
      /*  sessionsCtx.updateSession(id, updatedSession); */
      //update inbox state
      setInbox(
        currentSessions.filter((session) => {
          const updatedData = session.archived === false;
          return updatedData;
        })
      );
      //update archive state
      setArchive(
        currentSessions.filter((session) => {
          const updatedData = session.archived === true;
          return updatedData;
        })
      );
    } catch (error) {
      alert(error.message);
    }
  };

  const unArchiveHandler = (item) => {
    try {
      const id = item.id;
      const updatedSession = {
        ...item, // copy current session
        archived: false,
      };
      const uid = auth.currentUser.uid;
      const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
      update(sessionRef, updatedSession);
      /*    sessionsCtx.updateSession(id, updatedSession); */
      //update unarchived session list
      setArchive((preState) => {
        //remove the item from archived list
        preState.filter((session) => session.id !== item.id);
        return [...preState];
      });
    } catch (error) {
      alert(error.message);
    }
  };

  const selectSessionHandler = useCallback(
    (selectedItem) => {
      switch (selectedItem) {
        case "Sessions":
          setStream(inbox);
          break;
        case "Archive":
          setStream(archive);
          break;
      }
    },
    [selectSessionHandler, inbox, archive]
  );

  const selectFilterHandler = (selectedItem) => {
    //filter the session list
    switch (selectedItem) {
      case "ABC":
        // Use the Array.sort() method to sort the list alphabetically in ascending order
        const sortedList = stream.sort((a, b) => {
          return a.title.localeCompare(b.title);
        });
        setStream((preState) => {
          return [...sortedList];
        });
        break;
      case "CBA":
        // Use the Array.sort() method to sort the list alphabetically in descending order
        const sortedList2 = stream.sort((a, b) => {
          return b.title.localeCompare(a.title);
        });
        setStream((preState) => {
          return [...sortedList2];
        });
        break;
      case "Latest Date":
        // Use the Array.sort() method to sort the list by date in descending order
        const sortedList3 = stream.sort((a, b) => {
          return b.date.localeCompare(a.date);
        });
        setStream((preState) => {
          return [...sortedList3];
        });
        break;
      case "Earliest Date":
        // Use the Array.sort() method to sort the list by date in ascending order
        const sortedList4 = stream.sort((a, b) => {
          return a.date.localeCompare(b.date);
        });
        setStream((preState) => {
          return [...sortedList4];
        });
        break;
    }
  };

  const renderSessionItem = useCallback(({ item }) => {
    return (
      <Pressable
        /*         style={({ pressed }) => pressed && styles.pressed} */
        onPress={onPressHandler.bind(null, item)}
        key={item.id}
      >
        <SessionComponent
          key={item.id}
          title={item.title}
          identifier={item.identifier}
          date={item.date}
          rightSwipeActions={rightSwipeActions.bind(null, item)}
          smed={item.smed}
          endTime={item.endTime}
        />
      </Pressable>
    );
  }, []);

  return (
    <View style={styles.container}>
      <View style={styles.menuRow}>
        <SelectBtn
          data={sortList}
          onSelect={(item) => selectFilterHandler(item)}
        />
        <SelectBtn
          data={sessionList}
          onSelect={(item) => selectSessionHandler(item)}
        />
      </View>
      <FlatList
        data={stream}
        renderItem={renderSessionItem}
        keyExtractor={(item) => item.id}
        extraData={stream}
      />
    </View>
  );
}

export default SessionStream;

已尝试的解决方法

  • 使用ChatGPT排查问题
  • 更新全局会话状态以触发重渲染
  • 通过对象或扩展运算符更新本地状态
  • 为FlatList添加extraData属性
  • 移除useCallback确保无阻塞

问题修复方案

1. 修复FlatList重渲染不及时问题

核心问题:

  • useCallback依赖项错误,导致回调函数未捕获最新状态;
  • renderSessionItem依赖为空,无法感知onPressHandler和rightSwipeActions的变化;
  • Loading逻辑错误,setTimeout无法延迟组件渲染。

修复代码:

// 修正Loading逻辑
if (isFetching) {
  return <LoadingOverlay />;
}

// 修正onPressHandler依赖
const onPressHandler = useCallback(
  (item) => {
    navigation.navigate("Detail", {
      sessionID: item.id,
    });
  },
  [navigation] // 依赖navigation而非自身
);

// 修正rightSwipeActions依赖
const rightSwipeActions = useCallback(
  (item, swipeAnimatedValue) => {
    // ...原代码不变
  },
  [archiveHandler, unArchiveHandler, deleteHandler] // 依赖实际用到的处理函数
);

// 修正deleteHandler依赖
const deleteHandler = useCallback(
  (item) => {
    // ...原代码不变
  },
  [sessionsCtx] // 依赖sessionsCtx而非自身
);

// 修正renderSessionItem依赖
const renderSessionItem = useCallback(({ item }) => {
  // ...原代码不变
}, [onPressHandler, rightSwipeActions]); // 添加实际依赖

// 修正selectSessionHandler依赖
const selectSessionHandler = useCallback(
  (selectedItem) => {
    // ...原代码不变
  },
  [inbox, archive] // 移除循环依赖的selectSessionHandler
);

2. 修复滑动归档/取消归档后条目不更新问题

核心问题:

  • 注释掉了全局context的更新逻辑,导致currentSessions未同步最新状态;
  • 手动更新inbox/archive时使用旧的currentSessions值;
  • unArchiveHandler中setArchive逻辑错误,filter结果未返回。

修复代码:

// 修正archiveHandler
const archiveHandler = (item) => {
  try {
    const id = item.id;
    const updatedSession = {
      ...item,
      archived: true,
    };
    const uid = auth.currentUser.uid;
    const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
    update(sessionRef, updatedSession);
    // 恢复全局状态更新
    sessionsCtx.updateSession(id, updatedSession);
    // 移除手动更新inbox/archive的代码,交给useEffect自动处理
  } catch (error) {
    alert(error.message);
  }
};

// 修正unArchiveHandler
const unArchiveHandler = (item) => {
  try {
    const id = item.id;
    const updatedSession = {
      ...item,
      archived: false,
    };
    const uid = auth.currentUser.uid;
    const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
    update(sessionRef, updatedSession);
    // 恢复全局状态更新
    sessionsCtx.updateSession(id, updatedSession);
    // 移除错误的手动更新逻辑
  } catch (error) {
    alert(error.message);
  }
};

// 修正inbox/archive的useEffect依赖
useEffect(() => {
  setInbox(
    currentSessions.filter((session) => {
      return session.archived === false || session.archived === undefined;
    })
  );
  setArchive(
    currentSessions.filter((session) => {
      return session.archived === true;
    })
  );
}, [currentSessions]); // 仅依赖currentSessions,移除无关依赖

3. 修复过滤功能需点击两次生效问题

核心问题:

  • Array.sort()是原地修改数组,导致stream引用未变化,React无法检测到状态更新;
  • 第一次点击时setStream传入同一数组引用,不会触发重渲染,第二次点击才生成新引用。

修复代码:

const selectFilterHandler = (selectedItem) => {
  switch (selectedItem) {
    case "ABC":
      // 先复制数组再排序,避免原地修改
      const sortedList = [...stream].sort((a, b) => {
        return a.title.localeCompare(b.title);
      });
      setStream(sortedList);
      break;
    case "CBA":
      const sortedList2 = [...stream].sort((a, b) => {
        return b.title.localeCompare(a.title);
      });
      setStream(sortedList2);
      break;
    case "Latest Date":
      const sortedList3 = [...stream].sort((a, b) => {
        return b.date.localeCompare(a.date);
      });
      setStream(sortedList3);
      break;
    case "Earliest Date":
      const sortedList4 = [...stream].sort((a, b) => {
        return a
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 00:05:17