React useEffect内调用setState导致无限循环问题排查
React Firestore 实时监听更新状态触发无限循环修复
问题复现
开发中需要将Firebase拉取的用户列表分别存入两个状态:
- 本地状态
data:用于当前组件表格遍历渲染 - Context状态
userData:用于跨组件共享用户信息
代码运行后触发无限重渲染,原代码如下:
import { useState, useEffect, useContext } from 'react'; import { collection, onSnapshot } from 'firebase/firestore'; import { db, auth } from '../../firebase'; import { UserContext } from '../../Contexts/UserContext'; const Datatable = () => { const {userData, setUserData} = useContext(UserContext); const [data, setData] = useState([]); const [isEditing, setIsEditing] = useState(false); const [currentId, setCurrentId] = useState(""); useEffect(() => { const getUserData = () => { onSnapshot(collection(db, "users"), (snapshot) => { let list = []; snapshot.docs.forEach((doc) => { list.push({ id: doc.id, ...doc.data() }); setData(list); setUserData(list) }); }, (err) => { console.log(err); }); } getUserData(); }, []) }
问题原因
- 状态更新位置错误:你把
setData、setUserData写在了forEach循环内部,每遍历一条文档就触发两次状态更新,列表有N条数据就会触发2N次状态更新,产生大量无效渲染。 - 未清理实时监听器:
onSnapshot会创建持久化的实时数据订阅,原代码没有在组件卸载时取消订阅,会导致监听器重复创建,多个回调同时触发状态更新,最终形成无限循环。
修复方案
- 调整状态更新时机:等所有文档遍历完成、
list数组组装完毕后,再统一调用一次状态更新方法,避免循环内重复触发更新。 - 添加监听器清理逻辑:接收
onSnapshot返回的取消订阅函数,在useEffect的返回值中执行,组件卸载时自动销毁监听器,避免内存泄漏和重复触发。
修复后的完整代码:
import { useState, useEffect, useContext } from 'react'; import { collection, onSnapshot } from 'firebase/firestore'; import { db, auth } from '../../firebase'; import { UserContext } from '../../Contexts/UserContext'; const Datatable = () => { const {userData, setUserData} = useContext(UserContext); const [data, setData] = useState([]); const [isEditing, setIsEditing] = useState(false); const [currentId, setCurrentId] = useState(""); useEffect(() => { // 拿到实时订阅的取消引用 const unsubscribe = onSnapshot(collection(db, "users"), (snapshot) => { const list = []; // 先遍历组装完整列表 snapshot.docs.forEach((doc) => { list.push({ id: doc.id, ...doc.data() }); }); // 列表组装完成后统一更新状态,仅触发一次渲染 setData(list); setUserData(list); }, (err) => { console.log(err); }); // 组件卸载时取消订阅 return () => unsubscribe(); }, []) }
补充优化
如果修复后仍存在不必要的重复渲染,检查UserContext对应的Provider代码,不要直接给value传字面量对象,否则每次Provider重渲染都会生成新的对象引用,强制所有消费Context的组件重渲染。可以用useMemo缓存context值:
// UserContext Provider 侧代码参考 import { useState, useMemo } from 'react'; export const UserContextProvider = ({children}) => { const [userData, setUserData] = useState([]); // 缓存context值,仅当userData变化时才更新引用 const contextValue = useMemo(() => ({ userData, setUserData }), [userData]); return ( <UserContext.Provider value={contextValue}> {children} </UserContext.Provider> ) }
内容的提问来源于stack exchange,提问作者Prakash Subba
相关产品推荐
相关产品推荐

