NextJS结合Context与Firestore预渲染报错问题求助
问题分析与解决方案
核心错误原因
getStaticProps中调用useProductContext:React Hooks仅允许在React组件或自定义Hooks的顶层调用,getStaticProps是构建/服务器端的独立执行函数,不属于组件渲染流程,因此会抛出useContext相关错误。- ProductContext数据获取逻辑错误:
- 在组件外部使用
onSnapshot异步监听Firestore,同步赋值的paintingsData始终为空数组; - 在
ProductProvider中直接调用setProducts,会触发无限重渲染(每次组件渲染都会执行setProducts,进而触发新一轮渲染); - 组件外重复初始化Firebase应用,可能引发
Firebase App named '[DEFAULT]' already exists错误。
- 在组件外部使用
- 预渲染与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
相关产品推荐
相关产品推荐

