React自定义Hook引发重复渲染(Maximum Depth Exceeded)问题排查
解决React自定义Hook结合RTK Query的无限重渲染问题
问题根源
你的代码陷入无限重渲染循环的核心原因有两点:
- RTK Query返回的
data引用不稳定:即使服务器返回的数据内容没有变化,RTK Query每次返回的data对象/数组可能是新的引用(尤其是组件重渲染时,若查询参数引用变化或缓存机制细节导致)。 - 无意义的状态更新触发循环:你在
useEffect中调用setTransactions([]),哪怕当前transactions已经是空数组。由于[]是新创建的数组引用,React会判定状态发生变化,触发组件重渲染。重渲染后又会重新执行useGetTransactionsQuery,得到新的data引用,再次触发useEffect,形成无限循环,最终导致Maximum Depth Exceeded错误。
另外你提到的“首次加载后isLoading始终为true”,大概率是混淆了RTK Query的isLoading和isFetching:isLoading仅在首次请求未完成时为true,后续分页请求应该用isFetching判断加载状态。
解决方案
方案1:移除不必要的本地状态同步
如果不需要对RTK Query返回的数据做本地聚合(比如分页合并),直接使用RTK Query提供的data即可,完全不需要本地transactions状态:
export default function useTransactions(accountId) { const [page, setPage] = useState(1); const { data = { data: [] }, isSuccess, isFetching, // 用isFetching判断后续加载状态 } = useGetTransactionsQuery({ accountId, page }); const handleMoreTransactionsClick = () => { setPage(prev => prev + 1); }; const handleDeleteTransaction = (transactionId) => { // 调用你的删除mutation,RTK Query会自动更新缓存 // const [deleteTransaction] = useDeleteTransactionMutation(); // deleteTransaction(transactionId); }; return { transactions: data.data, handleMoreTransactionsClick, handleDeleteTransaction, isLoading: isFetching }; }
方案2:正确实现分页数据聚合(保留本地状态)
如果需要将多页数据聚合到本地transactions中,需要优化useEffect的触发条件,避免无意义的状态更新:
export default function useTransactions(accountId) { const [page, setPage] = useState(1); const [transactions, setTransactions] = useState([]); const { data = { data: [] }, isSuccess, isFetching, isError } = useGetTransactionsQuery({ accountId, page }); // 仅当新页数据加载成功,且数据内容有变化时,合并到本地状态 useEffect(() => { if (isSuccess && data.data.length > 0) { // 使用函数式更新,确保基于最新的prevState操作 setTransactions(prev => { // 避免重复添加相同数据(可选,根据接口返回逻辑调整) const newIds = new Set(data.data.map(item => item.id)); return [...prev, ...data.data.filter(item => !prev.some(p => p.id === item.id))]; }); } }, [data.data, isSuccess]); // 依赖具体的data.data,而非整个data对象 const handleMoreTransactionsClick = () => { if (!isFetching) { setPage(prev => prev + 1); } }; const handleDeleteTransaction = (transactionId) => { // 调用删除mutation后,本地状态同步删除(或依赖RTK Query缓存自动更新) setTransactions(prev => prev.filter(item => item.id !== transactionId)); // const [deleteTransaction] = useDeleteTransactionMutation(); // deleteTransaction(transactionId); }; return { transactions, handleMoreTransactionsClick, handleDeleteTransaction, isLoading: isFetching }; }
方案3:修复无意义的空数组赋值问题
针对你简化代码中的场景,如果确实需要重置transactions为空数组,需要添加条件判断,避免重复触发状态更新:
useEffect(() => { // 仅当当前transactions不是空数组时,才执行重置 if (transactions.length !== 0) { setTransactions([]); } }, [data, transactions]);
额外注意事项
- RTK Query的查询参数尽量使用稳定的引用:比如将
{accountId, page}用useMemo缓存,避免每次组件渲染时创建新对象导致不必要的缓存检查:const queryArg = useMemo(() => ({ accountId, page }), [accountId, page]); const { data, isSuccess } = useGetTransactionsQuery(queryArg); - 利用RTK Query的缓存标签(
providesTags/invalidatesTags)自动管理数据更新,比如删除交易后,让RTK Query自动重新获取数据,减少本地状态的手动维护。
内容的提问来源于stack exchange,提问作者maganthro
相关产品推荐
相关产品推荐

