Snipcart items count reseting to default on every new page load

I’m building a site with Gatsby and encountering an issue where I follow the snipcart documentation to add the items count to a div. It displays fine when I add an item to the cart, but the issue I encounter is if I then navigate to another page the display will go to the default in this case “0” even though the items are still in the cart. If I do a soft refresh on this new page the div will display the number of items in the cart properly. Can anyone advise on how best to remedy this? Thanks so much. Below is the code snippet for the div. I’m using the gatsby-plugin-snipcart-advanced with Gatsby.

          <div className="header__cart__cartNumber">
            <span className="snipcart-items-count">0</span>
          </div>

Hi there,

This is a common problem with SPAs, where placeholder elements like the items count get re-rendered by your app at runtime. When that occurs, the Snipcart app loses its bindings to them since they get replaced by new DOM elements.

One way to avoid this problem is to place these elements in a component that does not get re-rendered on page transitions.

If that somehow isn’t possible for you, one other option is to subscribe to store mutations and handle the count update yourself. We recently made a quick proof of concept, a React component that uses our JavaScript SDK:

import { useEffect } from 'react';
import { useState } from 'react';

const ItemsCount = () => {
	const [itemsCount, setItemsCount] = useState(0);

	useEffect(() => {
		const Snip = window.Snipcart;
		const initialState = Snip.store.getState();
		setItemsCount(initialState.cart.items.count);

		const unsubscribe = Snip.store.subscribe(() => {
			const newState = Snip.store.getState();
			setItemsCount(newState.cart.items.count);
		});

		return () => unsubscribe();
	}, [setItemsCount]);

	return (
	    <div className="snipcart-checkout">Cart (<span>{itemsCount}</span>)</div>
	);
}

export default ItemsCount;

I hope this helps!

Thanks so much Francis!

I’ll look into that subscribe option.

I’m a bit new to SPAs what would be a component that does not get re rendered on page transitions?

In a lot of applications you typically have some kind of a “Layout” component that will hold maybe your navigation menu, footer, and other visual elements persistent across pages. This component is generally rendered only once when your application boots. This is the kind of place you’ll be able to safely use our placeholder elements.

Ah gotcha, thanks so much Francis! Going to give it a shot storing it in the Layout component.

1 Like

Wooot! Am running NextJS and this is exactly what I needed! Muchas Gracias!

1 Like

Just following up on this solution.

In production I noticed that trying to utilize the useEffect solution documented here was failing and ‘window.Snipcart’ was undefined at the time the useEffect ran. Took some time to adjust the solution but here was my addition.

Note: This is for React/NextJS

  1. Create a context/provider to load the snipcart script if not loaded
import React, { createContext, useContext, useState, useEffect } from "react";

// Context.
export const SnipcartContext = createContext();

// Create a custom hook to use the context.
export const useSnipcartContext = () => useContext(SnipcartContext);

// Provider of context.
const SnipcartProvider = ({ children }) => {
  const [hasLoaded, setHasLoaded] = useState(false);
  const [isReady, setIsReady] = useState(false);

  /**
   * Extra security measure to check if the script has
   * already been included in the DOM
   */
  const scriptAlreadyExists = () =>
    document.querySelector("script#snipcart") !== null;

  /**
   * Append the script to the document.
   * Whenever the script has been loaded it will
   * set the isLoaded state to true.
   */
  const appendSnipcartScript = () => {
    const script = document.createElement("script");
    script.id = "snipcart";
    script.src = "https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.js";
    script.async = true;
    script.defer = true;
    script.crossOrigin = "anonymous";
    script.onload = () => setHasLoaded(true);
    document.body.append(script);
  };

  /**
   * Runs first time when component is mounted
   * and adds the script to the document.
   */
  useEffect(() => {
    if (!scriptAlreadyExists()) {
      appendSnipcartScript();
    }
  }, []);

  /**
   * Whenever the script has loaded
   * this will then set the isReady
   * state to true and passes that
   * through the context to the consumers.
   */
  useEffect(() => {
    if (hasLoaded === true) {
      setIsReady(true);
    }
  }, [hasLoaded]);

  return (
    <SnipcartContext.Provider value={{ isReady, hasLoaded }}>
      {children}
    </SnipcartContext.Provider>
  );
};

export default SnipcartProvider;
  1. Wrap your app with provider
import SnipcartProvider from "@context/snipcart";

function MyApp({ Component, pageProps }) {
  return (
    <SnipcartProvider>
      <div>
        <div
          hidden
          id="snipcart"
          data-api-key={process.env.PUBLIC_SNIPCART_API_KEY}
          data-config-modal-style="side"
        ></div>
        <Component {...pageProps} />
      </div>
    </SnipcartProvider>
  );
}

export default MyApp;
  1. Leverage useEffect solution while tying into ‘isReady’ from newly added context/provider
  import { useSnipcartContext } from "@context/snipcart";

  const { isReady } = useSnipcartContext();

  useEffect(() => {
    if (isReady) {
      const Snip = window.Snipcart;
      const initialState = Snip.store.getState();
      setItemsCount(initialState.cart.items.count);

      const unsubscribe = Snip.store.subscribe(() => {
        const newState = Snip.store.getState();
        setItemsCount(newState.cart.items.count);
      });

      return () => unsubscribe();
    }
  }, [setItemsCount, isReady]);

Implemented quickly so may need some tweaking but seems to have everything running smoothly again.

1 Like

Thank you so much for taking the time to contribute to this thread @smorgan :pray:

1 Like