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

NextJS结合Context与Firestore预渲染报错问题求助

问题分析与解决方案

核心错误原因

  1. getStaticProps中调用useProductContext:React Hooks仅允许在React组件或自定义Hooks的顶层调用,getStaticProps是构建/服务器端的独立执行函数,不属于组件渲染流程,因此会抛出useContext相关错误。
  2. ProductContext数据获取逻辑错误:
    • 在组件外部使用onSnapshot异步监听Firestore,同步赋值的paintingsData始终为空数组;
    • 在ProductProvider中直接调用setProducts,会触发无限重渲染(每次组件渲染都会执行setProducts,进而触发新一轮渲染);
    • 组件外重复初始化Firebase应用,可能引发Firebase App named '[DEFAULT]' already exists错误。
  3. 预渲染与Context的逻辑冲突:预渲染需要在构建时同步获取数据,但原代码依赖客户端Context异步拉取,导致预渲染阶段数据为空,触发页面报错。

具体修复步骤

1. 规范Firebase初始化

修改firebase/firebase.config.js,避免重复初始化应用:

import { initializeApp, getApps, getApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  // 你的Firebase配置内容
};

// 仅在无现有应用时初始化
const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };

2. 重构ProductContext,支持预渲染数据传入

修改components/context/ProductContext.js,让Context可以接收预渲染的初始商品数据,同时保留客户端 fallback 逻辑:

import { createContext, useContext, useState, useEffect } from "react";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../../firebase/firebase.config";

const ProductContext = createContext();

export function ProductProvider({ children, initialProducts = [] }) {
  const [cart, setCart] = useState([]);
  const [products, setProducts] = useState(initialProducts);
  const [total, setTotal] = useState(0);
  const [numberOfItems, setNumberOfItems] = useState(0);

  // 客户端 fallback:若预渲染无数据,在客户端拉取
  useEffect(() => {
    if (products.length === 0) {
      const fetchProducts = async () => {
        const querySnapshot = await getDocs(collection(db, "paintings"));
        const fetchedProducts = querySnapshot.docs.map(doc => ({
          ...doc.data(),
          id: doc.id
        }));
        setProducts(fetchedProducts);
      };
      fetchProducts();
    }
  }, [products]);

  // 实时计算购物车总价和商品数量
  useEffect(() => {
    let newTotal = 0;
    let itemCount = 0;
    cart.forEach(item => {
      newTotal += item.price * item.quantity;
      itemCount += item.quantity;
    });
    setTotal(newTotal);
    setNumberOfItems(itemCount);
  }, [cart]);

  // 购物车操作函数
  const addToCart = (product) => {
    setCart(prev => {
      const existingItem = prev.find(item => item.id === product.id);
      return existingItem
        ? prev.map(item => item.id === product.id ? {...item, quantity: item.quantity + 1} : item)
        : [...prev, {...product, quantity: 1}];
    });
  };

  const removeFromCart = (productId) => {
    setCart(prev => prev.filter(item => item.id !== productId));
  };

  const checkout = () => {
    setCart([]);
  };

  return (
    <ProductContext.Provider
      value={{
        cart,
        products,
        total,
        numberOfItems,
        addToCart,
        removeFromCart,
        checkout
      }}
    >
      {children}
    </ProductContext.Provider>
  );
}

export function useProductContext() {
  return useContext(ProductContext);
}

3. 修改首页(index.js),实现预渲染

在首页的getStaticProps中直接从Firestore拉取数据,传给页面并初始化Context:

import { useProductContext } from "../components/context/ProductContext";
import ProductItem from "../components/ProductItem";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../firebase/firebase.config";

export default function ProductList() {
  const { addToCart, products } = useProductContext();

  if (!products || products.length === 0) {
    return <div>Loading...</div>;
  }

  return (
    <div className="App">
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          addToCart={addToCart}
        />
      ))}
    </div>
  );
}

export async function getStaticProps() {
  // 构建时同步拉取所有商品数据
  const querySnapshot = await getDocs(collection(db, "paintings"));
  const initialProducts = querySnapshot.docs.map(doc => ({
    ...doc.data(),
    id: doc.id
  }));

  return {
    props: { initialProducts },
    revalidate: 86400 // 可选:24小时自动重新生成页面
  };
}

4. 修改_app.js,传递预渲染数据给Context

import { ProductProvider } from "../components/context/ProductContext";
import "../styles/styles.css";

export default function App({ Component, pageProps }) {
  return (
    <ProductProvider initialProducts={pageProps.initialProducts || []}>
      <Component {...pageProps} />
    </ProductProvider>
  );
}

5. 修复动态路由页面(/pages/products/[id].js)

在getStaticProps中直接拉取数据,动态生成路径:

import { useProductContext } from "../../components/context/ProductContext";
import { useRouter } from "next/router";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../../firebase/firebase.config";

export default function ProductDetails() {
  const { products, addToCart } = useProductContext();
  const router = useRouter();
  const { id } = router.query;

  if (!products || products.length === 0) {
    return <div>Loading...</div>;
  }

  const product = products.find(p => p.id === id);
  if (!product) return <div>Product not found</div>;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>${product.price}</p>
      <button onClick={() => addToCart(product)}>Add to Cart</button>
      {/* 其他商品详情内容 */}
    </div>
  );
}

export async function getStaticProps() {
  const querySnapshot = await getDocs(collection(db, "paintings"));
  const initialProducts = querySnapshot.docs.map(doc => ({
    ...doc.data(),
    id: doc.id
  }));

  return {
    props: { initialProducts },
    revalidate: 86400
  };
}

export async function getStaticPaths() {
  // 动态获取所有商品ID生成路径,无需手动编写
  const querySnapshot = await getDocs(collection(db, "paintings"));
  const paths = querySnapshot.docs.map(doc => ({
    params: { id: doc.id }
  }));

  return { paths, fallback: true };
}

验证修复

运行npm run build检查是否还有预渲染错误,再执行npm run start验证页面渲染、购物车功能是否正常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 10:10:15