我正在为购物车实现此自定义挂钩,其中基本上从异步存储中获取产品 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;
在 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;