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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 15:31:15