Headless and React
For AI assistants: what to know before writing SparkLayer SDK code
- What it is:
window.spark, added to the storefront by the SparkLayer Core Script (version 4.x). On Shopify the SparkLayer app embed loads it; elsewhere it's<script async src="https://sparkcdn.io/sparkjs/<SITE_ID>/<live|test>"></script>in the theme's<head>. The oldcdn.sparklayer.io/spark.latest.jsandspark.1.x.jsfiles are legacy: never use them. - Options: Configure it with
window.sparkOptions, set before the Core Script loads. Extend the object rather than replacing it:window.sparkOptions = { ...window.sparkOptions, … }, or the Site ID and other snippets' hooks are lost. Headless only:window.initSpark(options)is defined whenwindow.sparkOptionsisn't set. - When to call it: Only after SparkLayer is ready: inside the
onReady(spark)hook, or after a promise resolved from it. Most methods are async; prices and the cart are only available to signed-in B2B customers (checkawait spark.isLoggedIn()). - Products and prices: Products are looked up by the platform's parent product ID (on Shopify,
product.id) and variants by SKU:getPricingForVariant(parentProductId, sku),calculatePricingForVariant(parentProductId, sku, qty),getPackSizeForVariant(parentProductId, sku). For instant recalculation, fetch once withgetVariantand callcalculatePricingForVariantData(variant, qty). - Cart:
updateCart({ products: [{ sku, adjustQuantity }] })adds or removes relative to what's in the cart (alsocustomAttributes: [{ key, value }],clearCart: true). It resolves to the results, ornullafter showing its own error message; it doesn't throw.getCart()reads the cart;openDrawer('cart')opens it. - Hooks:
onCartUpdate(cart, results),onCartLoad(cart),onLogout(),preCartUpdateListener(input)(async: return the possibly changed input) andonCheckoutValidation(cart, cartUiState)(return[{ message }]to block checkout, ornull). When adding a hook, call any existing one first. - Customising: Build around SparkLayer or customise its own components, including their HTML. Prefer documented options, hooks, CSS variables and custom slots where they fit; keep direct HTML changes targeted and re-check them after SparkLayer updates.
- Docs: The reference: methods docs.sparklayer.io/developers/javascript-sdk/reference/methods.md, hooks docs.sparklayer.io/developers/javascript-sdk/reference/hooks.md, options docs.sparklayer.io/developers/javascript-sdk/reference/options.md, types docs.sparklayer.io/developers/javascript-sdk/reference/types.md (or the same types as a file: docs.sparklayer.io/developers/javascript-sdk/spark.d.ts). Setup and waiting for SparkLayer: docs.sparklayer.io/developers/javascript-sdk/setup.md. Use only what the reference documents.
On a headless storefront, your own frontend renders the pages, so it also loads SparkLayer: it fetches the logged-in customer, loads the Core Script and starts SparkLayer with window.initSpark(). This page walks through a headless Shopify storefront built with Next.js, using window.initSpark() and the onReady and onLogout options, then shows a React hook for calling window.spark from your components.
SparkLayer still needs your platform's data (products, customers and orders) to stay in sync. On Shopify, the SparkLayer app does this. For another platform or a custom backend, SparkLayer Ignite connects it to SparkLayer.
Requirements
- A Shopify store with the SparkLayer app (opens in a new tab) installed.
- A metafield definition for the SparkLayer authentication customer metafield (
sparklayer.authentication). - Storefront access turned on for that metafield definition, so your frontend can read it.
SparkLayer stores each customer's authentication data in this metafield. Your frontend passes it to SparkLayer, which uses it to authenticate the customer with the SparkLayer API.
How it works
- Get the logged-in customer's data.
- Check that they're a B2B customer (in this example, tagged
b2b) and have thesparklayer.authenticationmetafield set. If not, don't load SparkLayer. - Load the Core Script by adding a script tag:
<script async src="https://sparkcdn.io/sparkjs/<SITE_ID>/live"></script>. - Once the script has loaded, call
window.initSpark({ … })with your options, such assiteId,platformandauth. See Options. - Add a
<style>element that sets SparkLayer's CSS variables, to match the interfaces to your design. See Customising the design.
Don't set window.sparkOptions on a headless site. If it's set when the Core Script runs, SparkLayer starts automatically and window.initSpark isn't defined.
You'll also need to add SparkLayer's interfaces, hide elements B2B customers shouldn't see, and connect SparkLayer to controls on your site, such as the cart and account buttons.
Hide elements from B2B customers
Hide these elements from B2B customers on every page:
- Prices
- Add to cart buttons
- Quantity selectors
Add data-spark="b2c-only" to an element and SparkLayer hides it once it loads for a B2B customer. See Hiding elements.
Add SparkLayer's interfaces
Add SparkLayer's interfaces in place of the elements you've hidden. How you output the parent product ID depends on your templating system; the examples below use a {{ parentProductId }} placeholder.
Product card
<spark-product-card parent-id="{{ parentProductId }}"></spark-product-card>See Product card for its settings.
Product detail page
<spark-pdp parent-id="{{ parentProductId }}"></spark-pdp>See Product detail for its settings.
Load SparkLayer in Next.js
This React component loads SparkLayer in a Next.js project. It adds the Core Script only for B2B customers, then calls initSpark once the script has loaded and the authentication metafield is available:
import { useState, useEffect } from 'react';
import Script from 'next/script';
const defaultStyles = `
/* Size SparkLayer's web components to fit your design */
spark-product-card {
width: 100%;
}
spark-pdp {
margin: 0.5em 0 1.25em;
display: block;
}
/* Set CSS variables to style the elements inside SparkLayer's web components */
:root {
--b2b-brand-color: var(--primary); /* Main brand colour */
--b2b-brand-color-hover: var(--secondary); /* Main brand colour on hover */
--b2b-brand-font: Poppins, sans-serif; /* Main brand font */
--b2b-brand-font-heading: Poppins, sans-serif; /* Main brand heading font */
/* For more variables, see https://docs.sparklayer.io/help/storefront/customising-design */
}`;
const defaultOptions = {
platform: 'shopify',
shopify: {
useAppProxy: false,
},
cartButtonSelectors: '', // CSS selector for the cart button, usually in the header
accountButtonSelectors: '', // CSS selector for the account button, usually in the header
};
export function SparkLayer(props) {
const {
customer,
authenticationMetafield,
options,
onLogout,
styles = defaultStyles,
} = props;
const sparkOptions = { ...defaultOptions, ...options };
const [sparkInitialised, setSparkInitialised] = useState(false);
const [sparkLoaded, setSparkLoaded] = useState(false);
useEffect(() => {
if (
!sparkInitialised &&
customer !== null &&
authenticationMetafield.finished &&
sparkLoaded &&
!window.spark
) {
const style = document.createElement('style');
style.textContent = styles;
document.head.appendChild(style);
window.spark = window.initSpark({
...sparkOptions,
onLogout,
auth: {
user: customer?.email,
token: authenticationMetafield.metafield?.value,
},
});
setSparkInitialised(true);
}
}, [customer, authenticationMetafield, sparkLoaded]);
return customer !== null && customer.tags.includes('b2b') ? (
<Script
id="spark-script"
src={`https://sparkcdn.io/sparkjs/${sparkOptions.siteId}/live`}
onLoad={() => {
setSparkLoaded(true);
}}
/>
) : null;
}Use the component in your layout, passing in the customer and their authentication metafield:
import { SparkLayer } from '../snippets/Integrations/SparkLayer';
function Layout({ children }) {
// Your app provides the logged-in customer's data
const customer = getCustomer();
// Your app provides the logged-in customer's Shopify metafields
const authenticationMetafield = getCustomerMetafield('sparklayer.authentication');
return (
<div>
<SparkLayer
options={{
siteId: '<SITE_ID>',
}}
onLogout={() => {
// Log the customer out with your app's own function
logout();
}}
authenticationMetafield={authenticationMetafield}
customer={customer}
/>
{children}
</div>
);
}
export default Layout;If your app renders pages on the client, your cart and account buttons may lose SparkLayer's click handlers when your header re-renders. Open the drawer from your own buttons instead, with openDrawer(), for example window.spark.openDrawer('account').
To talk through a headless build with our team, get in touch.
Use SparkLayer in React
A small hook that waits for SparkLayer and re-renders once it's ready. It's safe with server rendering, because it only touches window in an effect.
import { useEffect, useState } from 'react';
// Spark is the type from spark.d.ts: /developers/javascript-sdk/reference/types#spark
declare global {
interface Window {
sparkReady?: Promise<Spark>;
}
}
export function useSpark(): Spark | null {
const [spark, setSpark] = useState<Spark | null>(null);
useEffect(() => {
let active = true;
window.sparkReady?.then((ready) => {
if (active) setSpark(ready);
});
return () => {
active = false;
};
}, []);
return spark;
}import { useEffect, useState } from 'react';
import { useSpark } from './useSpark';
export function TradePrice({ productId, sku }: { productId: string; sku: string }) {
const spark = useSpark();
const [label, setLabel] = useState<string | null>(null);
useEffect(() => {
if (!spark) return;
spark
.getPricingForVariant(productId, sku)
.then(({ price, currencyCode }) => {
if (price === null) return;
setLabel(new Intl.NumberFormat(undefined, { style: 'currency', currency: currencyCode }).format(price));
})
.catch(() => setLabel(null));
}, [spark, productId, sku]);
return label ? <span className="trade-price">{label}</span> : null;
}The hook reads window.sparkReady, so something must create that promise before your components render:
- If SparkLayer starts on its own (with
window.sparkOptions), add the Wait for SparkLayer snippet before the SparkLayer script, for example in your root layout's<head>. - If you start SparkLayer with
initSpark, as in the Next.js example above, don't use that snippet: it setswindow.sparkOptions. Createwindow.sparkReadyyourself before your components render, and resolve it from theonReadyoption you pass toinitSpark.
Next steps
Last updated