Firestore 无限获取错误

问题描述 投票:0回答:1

我正在为购物车实现此自定义挂钩,其中基本上从异步存储中获取产品 ID 和变体,然后从 Firestore 获取产品的所有信息。然后逻辑会同步获取的数据并显示它。我还实现了 Firestore 的实时功能。它以某种方式正常工作,但是,根据我输入的日志,获取似乎是无限的(基于日志)。

这是自定义挂钩:

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 [cart, setCart] = 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 [debouncedCart] = useDebounce(cart, 500);

  // Fetch cart from AsyncStorage once on mount
  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);
        setCart(parsedCart);
      } catch (error) {
        console.error('Failed to fetch cart:', error);
      } finally {
        setLoading(false);
      }
    };
    fetchCart();
  }, []);

  // Fetch product details in batches (lazy loading)
  const fetchProductDetails = useCallback(async () => {
    console.log('fetchProductDetails called');
    if (cart.length === 0 || isFetchingMore) {
      console.log('No products to fetch or already fetching more');
      return;
    }

    const productIds = [...new Set(cart.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);
    }
  }, [cart, lastVisible, isFetchingMore]);

  // Fetch initial product details when cart changes
  useEffect(() => {
    if (initialFetch.current) {
      console.log('Initial cart fetch, skipping fetchProductDetails');
      initialFetch.current = false;
    } else {
      fetchProductDetails();
    }
  }, [debouncedCart, fetchProductDetails]);

  // Sync cart items with product details
  useEffect(() => {
    console.log('Product details changed:', productDetails);
    if (Object.keys(productDetails).length === 0) return;

    setCart(prevCart =>
      prevCart.map(item => ({
        ...item,
        data: productDetails[item.productId],
      }))
    );
  }, [productDetails]);

  // Real-time updates for cart products
  useEffect(() => {
    console.log('Setting up real-time updates for cart products');
    const productIds = [...new Set(cart.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();
    };
  }, [cart, updatingCart]);

  const updateQuantity = useCallback(async (productId, variant, quantity) => {
    console.log('updateQuantity called:', productId, variant, quantity);
    if (quantity <= 0) return;

    updatingCart.current = true;
    setCart(prevCart => {
      const updatedCart = prevCart.map(item =>
        item.productId === productId && item.variant[0] === variant[0]
          ? { ...item, quantity }
          : item
      );
      console.log('Quantity changes:', updatedCart);
      AsyncStorage.setItem(CART_KEY, JSON.stringify(updatedCart));
      return updatedCart;
    });
    updatingCart.current = false;
  }, []);

  const removeProduct = useCallback(async (productId, variant) => {
    console.log('removeProduct called:', productId, variant);
    updatingCart.current = true;
    setCart(prevCart => {
      const updatedCart = prevCart.filter(
        item => !(item.productId === productId && item.variant[0] === variant[0])
      );
      AsyncStorage.setItem(CART_KEY, JSON.stringify(updatedCart));
      return updatedCart;
    });
    updatingCart.current = false;
  }, []);

  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;
reactjs react-native infinite-loop react-custom-hooks
1个回答
0
投票

在 React 中,

useRef
对象(在我们的例子中为
updatingCart
)在
current
属性更改时不会导致重新渲染。 通过在
updatingCart.current
的依赖数组中使用
useEffect
,可以确保效果是根据
updatingCart.current
的值而不是ref对象本身触发的。
(在下面的代码中查找注释
// <= this line is changed
) ).

此外,

isFetchingMore
状态在函数内发生变化,也是
fetchProductDetails
的依赖项,如果处理不当,可能会产生循环。但是,看起来您已经添加了防护措施,以防止在没有可获取的产品时执行。

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 [cart, setCart] = 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 [debouncedCart] = useDebounce(cart, 500);

  // Fetch cart from AsyncStorage once on mount
  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);
        setCart(parsedCart);
      } catch (error) {
        console.error('Failed to fetch cart:', error);
      } finally {
        setLoading(false);
      }
    };
    fetchCart();
  }, []);

  // Fetch product details in batches (lazy loading)
  const fetchProductDetails = useCallback(async () => {
    console.log('fetchProductDetails called');
    if (cart.length === 0 || isFetchingMore) {
      console.log('No products to fetch or already fetching more');
      return;
    }

    const productIds = [...new Set(cart.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);
    }
  }, [cart, lastVisible, isFetchingMore]);

  // Fetch initial product details when cart changes
  useEffect(() => {
    if (initialFetch.current) {
      console.log('Initial cart fetch, skipping fetchProductDetails');
      initialFetch.current = false;
    } else {
      fetchProductDetails();
    }
  }, [debouncedCart, fetchProductDetails]);

  // Sync cart items with product details
  useEffect(() => {
    console.log('Product details changed:', productDetails);
    if (Object.keys(productDetails).length === 0) return;

    setCart(prevCart =>
      prevCart.map(item => ({
        ...item,
        data: productDetails[item.productId],
      }))
    );
  }, [productDetails]);

  // Real-time updates for cart products
  useEffect(() => {
    console.log('Setting up real-time updates for cart products');
    const productIds = [...new Set(cart.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();
    };
  }, [cart, updatingCart.current]); // <= this line is changed

  const updateQuantity = useCallback(async (productId, variant, quantity) => {
    console.log('updateQuantity called:', productId, variant, quantity);
    if (quantity <= 0) return;

    updatingCart.current = true;
    setCart(prevCart => {
      const updatedCart = prevCart.map(item =>
        item.productId === productId && item.variant[0] === variant[0]
          ? { ...item, quantity }
          : item
      );
      console.log('Quantity changes:', updatedCart);
      AsyncStorage.setItem(CART_KEY, JSON.stringify(updatedCart));
      return updatedCart;
    });
    updatingCart.current = false;
  }, []);

  const removeProduct = useCallback(async (productId, variant) => {
    console.log('removeProduct called:', productId, variant);
    updatingCart.current = true;
    setCart(prevCart => {
      const updatedCart = prevCart.filter(
        item => !(item.productId === productId && item.variant[0] === variant[0])
      );
      AsyncStorage.setItem(CART_KEY, JSON.stringify(updatedCart));
      return updatedCart;
    });
    updatingCart.current = false;
  }, []);

  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;
© www.soinside.com 2019 - 2024. All rights reserved.