Skip to content

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 old cdn.sparklayer.io/spark.latest.js and spark.1.x.js files 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 when window.sparkOptions isn'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 (check await 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 with getVariant and call calculatePricingForVariantData(variant, qty).
  • Cart: updateCart({ products: [{ sku, adjustQuantity }] }) adds or removes relative to what's in the cart (also customAttributes: [{ key, value }], clearCart: true). It resolves to the results, or null after 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) and onCheckoutValidation(cart, cartUiState) (return [{ message }] to block checkout, or null). 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

  1. Get the logged-in customer's data.
  2. Check that they're a B2B customer (in this example, tagged b2b) and have the sparklayer.authentication metafield set. If not, don't load SparkLayer.
  3. Load the Core Script by adding a script tag: <script async src="https://sparkcdn.io/sparkjs/<SITE_ID>/live"></script>.
  4. Once the script has loaded, call window.initSpark({ … }) with your options, such as siteId, platform and auth. See Options.
  5. 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

Product list
<spark-product-card parent-id="{{ parentProductId }}"></spark-product-card>

See Product card for its settings.

Product detail page

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:

SparkLayer.jsx
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:

Layout.jsx
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.

useSpark.ts
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;
}
TradePrice.tsx
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 sets window.sparkOptions. Create window.sparkReady yourself before your components render, and resolve it from the onReady option you pass to initSpark.

Next steps

Was this page helpful?

Last updated