React Context API结合Firebase Promise请求时products加载为null问题
问题核心原因
你现在的代码有三个直接导致bug的问题:
- 对Firebase的
onSnapshotAPI理解完全错了:这个方法不是用来发请求拿一次性数据的,是注册一个长连接的实时监听器,你传进去的回调会在每次数据变化的时候触发。你现在给它套了个Promise,刚注册完监听、回调还没等Firebase返回数据触发的时候,就立刻resolve(myArray)了,这时候myArray就是个空数组,第一次给setProducts赋的值就是空的。 - 你看到的「打印
products.length是0,点开打印的数组却有1个对象」是浏览器控制台的通用特性:打印数组、对象这类引用类型值的时候,控制台不会立刻保存打印那一刻的快照,等你手动在控制台点开这个值的时候,它才会去读当前内存里这个引用指向的最新内容。后面Firebase的数据返回了,回调往myArray里push了内容,所以你点开的时候能看到数据,但打印的瞬间数组确实是空的,length为0完全符合当时的实际状态。 - 你虽然定义了
isLoading的加载状态,但既没有在数据获取流程里更新它,也没有把它透传到Context里,更没在渲染的时候做加载判断,自然做不到等数据加载完再渲染内容。
修复方案
1. 修正Context里的数据获取逻辑
不需要自己额外包Promise套async,直接在onSnapshot的回调里更新状态即可,同时记得更新加载状态、组件卸载时取消监听避免内存泄漏。
修正后的ProductContext.js代码:
import React, {createContext, useState, useEffect} from 'react'; import { collection, onSnapshot } from "firebase/firestore"; import {db} from "../firebase"; export const ProductsContext = createContext(); const ProductsContextProvider = ({children}) => { const [products, setProducts] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { // 注册实时监听,拿到取消监听的方法 const unsubscribe = onSnapshot(collection(db, 'products'), (snapshot) => { const productList = []; snapshot.forEach(doc => { // 记得把Firebase的文档id带上,避免列表渲染时key报错 productList.push({ id: doc.id, ...doc.data() }) }) setProducts(productList); // 数据赋值完成后关闭加载状态 setIsLoading(false); }) // 组件卸载时取消监听,防止内存泄漏 return () => unsubscribe(); }, []); return ( <ProductsContext.Provider value={{products, isLoading}}> { children } </ProductsContext.Provider> ); } export default ProductsContextProvider;
如果你不需要实时同步数据,只想进页面拉取一次商品列表就行,可以把
onSnapshot换成一次性取数的getDocs,对应写法:import { collection, getDocs } from "firebase/firestore"; // useEffect部分替换为 useEffect(() => { async function fetchProducts() { setIsLoading(true); const querySnapshot = await getDocs(collection(db, 'products')); const productList = []; querySnapshot.forEach(doc => { productList.push({id: doc.id, ...doc.data()}) }) setProducts(productList); setIsLoading(false); } fetchProducts(); }, [])
之前写的独立getProductData函数可以直接删掉,那个函数外层定义的空数组会因为闭包特性残留数据,多次调用会出现重复push商品的bug。
2. 渲染层加加载判断
useProducts.js不需要改动,直接在商品列表组件里拿到isLoading状态,加载过程中不渲染商品列表即可。
修正后的ProductsGrid.js核心逻辑:
import React from 'react'; import ProductItem from './ProductItem'; import styles from './ProductsGrid.module.scss'; import { useProducts } from '../../hooks/useProducts'; const ProductsGrid = () => { const {products, isLoading} = useProducts(); // 加载中先展示加载提示,不渲染商品列表 if (isLoading) { return <div className={styles.p__container}>商品加载中...</div> } return ( <div className={styles.p__container}> <div className="row"> <div className="col-sm-8"> <div className="py-3"> {products.length} Products </div> </div> <div className="col-sm-4"></div> </div> <div className={styles.p__grid}> { products.map(product => ( <ProductItem key={product.id} product={product}/> )) } </div> <div className={styles.p__footer}></div> </div> ); } export default ProductsGrid;
调试小提示
后续调试引用类型值(数组、对象)的时候,不要直接依赖控制台展开看到的内容判断执行时序,真要确认某一刻的具体值,可以打印的时候做一次深拷贝,比如console.log(JSON.parse(JSON.stringify(products))),或者直接打印基础类型值(比如length、具体的字符串/数字字段),拿到的才是执行到那行代码时的准确值。
内容的提问来源于stack exchange,提问作者wonderingwildcat
相关产品推荐
相关产品推荐

