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
- 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;
- 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;
- 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.