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

Firestore无限获取Bug求助:购物车自定义Hook异常排查

购物车自定义Hook无限循环问题排查与修复

核心循环链路

你的代码存在闭环触发的无限循环:

  1. productDetails 更新 → 触发同步cart的Effect,调用setCart修改cart状态
  2. cart状态变化 → 带动debouncedCart更新 → 触发依赖debouncedCart和fetchProductDetails的Effect,调用fetchProductDetails
  3. 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;

关键修复点说明

  1. 拆分cart状态:用rawCart存储核心数据,cart通过useMemo派生,避免因添加详情字段触发状态更新
  2. 优化依赖数组:移除fetchProductDetails中的isFetchingMore依赖,避免函数频繁重建;实时更新Effect移除不必要的ref依赖
  3. 修正异步操作:将AsyncStorage.setItem改为异步等待,确保状态更新完成后再存储
  4. 切断循环链路:通过派生数据替代状态修改,打破productDetails→cart→fetchProductDetails→productDetails的循环

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 23:20:54