Firestore无限获取Bug求助:购物车自定义Hook异常排查
购物车自定义Hook无限循环问题排查与修复
核心循环链路
你的代码存在闭环触发的无限循环:
productDetails更新 → 触发同步cart的Effect,调用setCart修改cart状态- cart状态变化 → 带动
debouncedCart更新 → 触发依赖debouncedCart和fetchProductDetails的Effect,调用fetchProductDetails fetchProductDetails执行中更新productDetails→ 回到第一步,循环往复
同时还有几个细节问题放大了这个循环:
具体问题分析
1. cart与productDetails的同步逻辑触发不必要的cart更新
你在同步cart和productDetails的Effect里,每次productDetails变化就重新映射cart数组,给每个item添加data字段。这会生成全新的cart数组引用,即使内容无实质变化,也会触发所有依赖cart的Effect/回调。
2. fetchProductDetails的依赖包含可变状态isFetchingMore
isFetchingMore是频繁修改的状态,把它放进useCallback的依赖数组,会导致每次isFetchingMore变化时,fetchProductDetails都生成新的函数实例,进而触发依赖它的Effect重复执行。
3. 实时更新Effect的依赖冗余
updatingCart是ref,它的变化不会触发Effect更新,不需要放进依赖数组。
4. updateQuantity/removeProduct中AsyncStorage调用时机错误
你在setCart的回调里直接调用AsyncStorage.setItem,但setItem是异步函数,应该放在回调外用await处理,避免状态未更新完成就写入存储。
修复方案
步骤1:优化cart与productDetails的同步逻辑
- 拆分cart状态:用
rawCart存储核心购物车元数据(productId、variant、quantity),展示用的带详情的cart通过useMemo派生,避免触发不必要的状态更新。
步骤2:调整fetchProductDetails的依赖
- 移除
isFetchingMore从依赖数组,函数内部直接读取最新状态即可,无需作为依赖。
步骤3:修正实时更新Effect的依赖
- 移除
updatingCart依赖,ref变化不会触发Effect更新。
步骤4:修复AsyncStorage异步调用问题
- 在
updateQuantity和removeProduct中,先更新cart状态,再异步调用AsyncStorage.setItem并等待完成。
修复后的完整代码
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import firestore from '@react-native-firebase/firestore'; import { useDebounce } from 'use-debounce'; const CART_KEY = 'CART_KEY'; const BATCH_SIZE = 5; const useCartProducts = () => { const [rawCart, setRawCart] = useState([]); // 仅存储核心购物车数据 const [loading, setLoading] = useState(true); const [productDetails, setProductDetails] = useState({}); const [lastVisible, setLastVisible] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false); const initialFetch = useRef(true); const updatingCart = useRef(false); const [debouncedRawCart] = useDebounce(rawCart, 500); // 从AsyncStorage获取购物车数据 useEffect(() => { const fetchCart = async () => { try { const storedCart = await AsyncStorage.getItem(CART_KEY); const parsedCart = storedCart ? JSON.parse(storedCart) : []; console.log('Cart Items From AsyncStorage:', parsedCart); setRawCart(parsedCart); } catch (error) { console.error('Failed to fetch cart:', error); } finally { setLoading(false); } }; fetchCart(); }, []); // 批量获取商品详情 const fetchProductDetails = useCallback(async () => { console.log('fetchProductDetails called'); if (rawCart.length === 0 || isFetchingMore) { console.log('No products to fetch or already fetching more'); return; } const productIds = [...new Set(rawCart.map(item => item.productId))]; if (productIds.length === 0) { console.log('No product IDs found in cart'); return; } setIsFetchingMore(true); console.log('Fetching product details for IDs:', productIds); let query = firestore() .collection('products') .where(firestore.FieldPath.documentId(), 'in', productIds) .limit(BATCH_SIZE); if (lastVisible) { query = query.startAfter(lastVisible); } try { const querySnapshot = await query.get(); if (!querySnapshot.empty) { const details = {}; querySnapshot.forEach(doc => { details[doc.id] = doc.data(); }); setProductDetails(prevDetails => ({ ...prevDetails, ...details })); setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1]); console.log('Fetched product details:', details); } else { console.log('No more products to fetch'); } } catch (error) { console.error('Error fetching product details:', error); } finally { setIsFetchingMore(false); } }, [rawCart, lastVisible]); // 移除isFetchingMore依赖 // 当购物车变化时获取商品详情 useEffect(() => { if (initialFetch.current) { console.log('Initial cart fetch, skipping fetchProductDetails'); initialFetch.current = false; } else { fetchProductDetails(); } }, [debouncedRawCart, fetchProductDetails]); // 通过useMemo派生带详情的购物车数据,避免修改状态 const cart = useMemo(() => { return rawCart.map(item => ({ ...item, data: productDetails[item.productId] || null, })); }, [rawCart, productDetails]); // 实时更新商品详情 useEffect(() => { console.log('Setting up real-time updates for cart products'); const productIds = [...new Set(rawCart.map(item => item.productId))]; if (productIds.length === 0) return; const unsubscribe = firestore() .collection('products') .where(firestore.FieldPath.documentId(), 'in', productIds) .onSnapshot(snapshot => { if (updatingCart.current) return; const changes = {}; snapshot.forEach(doc => { changes[doc.id] = doc.data(); }); console.log('Real-time changes:', changes); setProductDetails(prevDetails => ({ ...prevDetails, ...changes })); }); return () => { console.log('Cleaning up real-time updates'); unsubscribe(); }; }, [rawCart]); // 移除updatingCart依赖 const updateQuantity = useCallback(async (productId, variant, quantity) => { console.log('updateQuantity called:', productId, variant, quantity); if (quantity <= 0) return; updatingCart.current = true; try { const updatedCart = rawCart.map(item => item.productId === productId && item.variant[0] === variant[0] ? { ...item, quantity } : item ); setRawCart(updatedCart); await AsyncStorage.setItem(CART_KEY, JSON.stringify(updatedCart)); console.log('Quantity changes:', updatedCart); } catch (error) { console.error('Failed to update quantity:', error); } finally { updatingCart.current = false; } }, [rawCart]); const removeProduct = useCallback(async (productId, variant) => { console.log('removeProduct called:', productId, variant); updatingCart.current = true; try { const updatedCart = rawCart.filter( item => !(item.productId === productId && item.variant[0] === variant[0]) ); setRawCart(updatedCart); await AsyncStorage.setItem(CART_KEY, JSON.stringify(updatedCart)); console.log('Product removed:', updatedCart); } catch (error) { console.error('Failed to remove product:', error); } finally { updatingCart.current = false; } }, [rawCart]); const totalPrice = useMemo( () => cart.reduce( (acc, item) => acc + item.quantity * parseFloat(item.variant[1].price), 0 ), [cart] ); console.log('Total price:', totalPrice); return { cart, loading, updateQuantity, removeProduct, totalPrice, loadMoreProducts: fetchProductDetails, }; }; export default useCartProducts;
关键修复点说明
- 拆分cart状态:用
rawCart存储核心数据,cart通过useMemo派生,避免因添加详情字段触发状态更新 - 优化依赖数组:移除
fetchProductDetails中的isFetchingMore依赖,避免函数频繁重建;实时更新Effect移除不必要的ref依赖 - 修正异步操作:将
AsyncStorage.setItem改为异步等待,确保状态更新完成后再存储 - 切断循环链路:通过派生数据替代状态修改,打破
productDetails→cart→fetchProductDetails→productDetails的循环
内容的提问来源于stack exchange,提问作者Josh Lsc
相关产品推荐
相关产品推荐

