reactjsgoogle-cloud-firestorenext.jsreact-context

Error prerendering NextJS with Context and Firestore


I am trying to convert my React simple eCommerce app to NextJS but am having problems with figuring out how to prerender my product data while using Context. The thing is, I do not need a dynamically updating system like with product quantity based on orders because this is just a demo app, so I should be able to just prerender everything and use getStaticProps and getStaticPaths, right? But I am having errors such as:

Error occurred prerendering page ___. (every page)

TypeError: Cannot read properties of null (reading 'useContext') (or useState)

The way this app works is I take in a Firestore database collection of a dozen products, then set them in the 'products' state in the ProductContext, to which I then access the context in the ProductList page to just map out the data to ProductItem components. Since any updates in item should just be through Context state, that should just be client-side right? cart, addToCart, checkout, etc. just use state from the context. I have also tried using getServerSideProps(). Also the UserContext NextJS should just access Firestore in the ProductContext on build to get the products, then be able to map them out and even use getStaticPaths on a ProductDetails component to use dynamic pathing for each item (/pages/products/[id]). But it is having issues prerendering each of these pages, ie.

Error occurred prerendering page "/products/1"

file structure:

-.next
-.vscode
-components
  -context
    -ProductContext
  -firebase
    -firebase.config.js
  -CartItem.js
  -Navbar.js
  -ProductItem.js
-node_modules
-pages
  -products
    -[id].js
  -_app.js
  -_document.js
  -about.js
  -cart.js
  -index.js
-public
-styles
  -styles.css
etc.

_app.js:

export default function App({ Component, pageProps }) {
  return (  
    <div>
      <ProductProvider>
          <Component {...pageProps} />
      </ProductProvider>
    </div>
    )
}

ProductContext.js

  const app = initializeApp(firebaseConfig);
  const db = getFirestore(app);
  const paintingsRef = collection(db, 'paintings')
  let paintings = []
  onSnapshot(paintingsRef, (snapshot) => {
    snapshot.docs.forEach((doc) => {
      paintings.push({ ...doc.data(), id: doc.id })
    })
    console.log(paintings)
    return paintings
  })
  const paintingsData = paintings

  
const ProductContext = createContext()

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

  ... (eCommerce functions)

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

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

index.js (list of products)

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

    return(
        <>
        <div className="App">
             products.map to <ProductItem /> component etc HTML
        </div>
    </>
    )
}

/pages/products/[id] for dynamic routing:

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

    return(
    <div>
    {
    products.filter((product) => product.id === id)
    .map((product, index) => (

        etc, HTML
     }
     </div>
)}

export async function getStaticProps() {
    const { products, addToCart } = useProductContext()
    return {
        props: {
            products: products,
            addToCart: addToCart
        }
    };
}

export async function getStaticPaths() {
    return {
        paths: [
            { params: { id: '1'} },
            { params: { id: '2'} },
            { params: { id: '3'} },
            { params: { id: '4'} },
            { params: { id: '5'} },
            { params: { id: '6'} },
            { params: { id: '7'} },
            { params: { id: '8'} },
            { params: { id: '9'} },
            { params: { id: '10'} },
            { params: { id: '11'} },
            { params: { id: '12'} },    
        ],
        fallback: true,
    }
}

Any help appreciated, and let me know if I can provide more info.


Solution

  • You can't use React Context in server because this feature is only available on the client.

    In order to fix your issue, you should do this:

    1. Get your required data from database in getStaticProps.

    2. Pass the data to your page component.

    3. Set your context values with the props that you've passed from the server.

    And also beside this point, your code has some problems.

    1. You're using getStaticProps which is primarily used inside a page to fetch data at build time but you're using onSnapshot method which listen to a doc for the changes. I think it's better to use getServerSideProps instead.

    2. You're writing your onSnapshot logic outside of the context component. So every time the data gets updated on the database, only your paintingsData gets updated but your context value won't be updated because your component does not re-render.

    So hope it helps:

    ProductContext.js

    const app = initializeApp(firebaseConfig);
    const db = getFirestore(app);
      
    const ProductContext = createContext();
    
    export function ProductProvider({ children }) {
      const [cart, setCart] = useState([])
      const [products, setProducts] = useState()
      const [total, setTotal] = useState(0)
      const [numberOfItems, setNumberOfItems] = useState(0)
    
      useEffect(() => {
        const paintingsRef = collection(db, 'paintings')
        onSnapshot(paintingsRef, (snapshot) => {
          const paintings = [];
          snapshot.docs.forEach((doc) => {
            paintings.push({ ...doc.data(), id: doc.id })
          });
          setProducts(paintings)
          return paintings
        });
      }, []);
    
      ... (eCommerce functions)
    
      return (
        <ProductContext.Provider
          value={{
            cart: cart,
            setProducts: setProducts,
            total: total,
            numberOfItems: numberOfItems,
            addToCart: addToCart,
            removeFromCart: removeFromCart,
            checkout: checkout,
          }}
        >
          {children}
        </ProductContext.Provider>
      )
    }
    
    export function useProductContext() {
      return useContext(ProductContext)
    }
    

    /pages/products/[id] for dynamic routing (with getStaticProps because you can't use getStaticPaths with getServerSideProps)

    export default function ProductDetails({ products }) {
      const { setProducts } = useProductContext()
      const router = useRouter()
      const { id } = router.query
    
      useEffect(() => {
        setProducts(products);
      }, [products]);
    
      return(
      <div>
      {
      products.filter((product) => product.id === id)
      .map((product, index) => (
    
          etc, HTML
       }
       </div>
    )}
    
    export async function getStaticProps() {
      const app = initializeApp(firebaseConfig);
      const db = getFirestore(app);
      const paintingsRef = collection(db, 'paintings');
      const snapshot = await getDocs(paintingsRef);
      const paintings = [];
      snapshot.docs.forEach((doc) => {
        paintings.push({ ...doc.data(), id: doc.id })
      });
      return {
        props: {
          products: paintings
        }
      };
    }
    
    export async function getStaticPaths() {
      return {
        paths: [
          { params: { id: '1'} },
          { params: { id: '2'} },
          { params: { id: '3'} },
          { params: { id: '4'} },
          { params: { id: '5'} },
          { params: { id: '6'} },
          { params: { id: '7'} },
          { params: { id: '8'} },
          { params: { id: '9'} },
          { params: { id: '10'} },
          { params: { id: '11'} },
          { params: { id: '12'} },    
        ],
        fallback: true,
      }
    }