# Headless and React

URL: https://docs.sparklayer.io/developers/javascript-sdk/headless

Run SparkLayer on a headless storefront: load the Core Script, start it with initSpark, authenticate the customer, and use it from Next.js and React.

> **For AI assistants:** what to know before writing SparkLayer JavaScript 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 https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md, hooks https://docs.sparklayer.io/developers/javascript-sdk/reference/hooks.md, options https://docs.sparklayer.io/developers/javascript-sdk/reference/options.md, types https://docs.sparklayer.io/developers/javascript-sdk/reference/types.md (or the same types as a file: https://docs.sparklayer.io/developers/javascript-sdk/spark.d.ts). Setup and waiting for SparkLayer: https://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](https://docs.sparklayer.io/ignite.md) connects it to SparkLayer. 

## Requirements

- **A Shopify store** with the [SparkLayer app](https://apps.shopify.com/sparklayer) 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](https://docs.sparklayer.io/developers/javascript-sdk/reference/options.md).
5. Add a `<style>` element that sets SparkLayer's CSS variables, to match the interfaces to your design. See [Customising the design](https://docs.sparklayer.io/help/storefront/customising-design.md).

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](https://docs.sparklayer.io/developers/frontend.md#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

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

See [Product card](https://docs.sparklayer.io/help/storefront/interfaces/product-card.md) for its settings.

### Product detail page

```html title="Product detail page"
<spark-pdp parent-id="{{ parentProductId }}"></spark-pdp>
```

See [Product detail](https://docs.sparklayer.io/help/storefront/interfaces/product-detail.md) 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:

```jsx title="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:

```jsx title="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()`](https://docs.sparklayer.io/developers/javascript-sdk/reference/methods.md#opendrawer), for example `window.spark.openDrawer('account')`.

To talk through a headless build with our team, [get in touch](https://docs.sparklayer.io/help/support.md).

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

```tsx title="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;
}
```

```tsx title="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](https://docs.sparklayer.io/developers/javascript-sdk/setup.md#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

- [Set up the SDK](https://docs.sparklayer.io/developers/javascript-sdk/setup.md): How the SDK loads, how to wait for it to be ready, and manual initialisation.
- [SDK reference](https://docs.sparklayer.io/developers/javascript-sdk/reference.md): Every option you can pass to initSpark, plus each hook, method and type.
- [Frontend integration](https://docs.sparklayer.io/developers/frontend.md): The Core Script, the product page interfaces and hiding elements from B2B customers.
