Frontend IntegrationFrameworksReact Integration

React Integration

React integration guide with basic (full-page and modal) and advanced widget methods for Sophi.

This page is organized in two levels:

  • Basic setup: copy-paste patterns that customers can run with minimal edits
  • Advanced setup: optional runtime controls (sendToggleHistory, sendProductReply, triggerProductReply)

For GTM script loading and script-load failures, use SDK GTM troubleshooting.

Required in every integration

No matter which UI pattern you use (full page or modal), implement all three:

  1. Handle add_to_cart
  2. Handle send_to_checkout
  3. Call widget.updateUserCart(cart) when cart changes

Basic setup

1) Access object from window

If you load SDK via GTM or script tag:

const SophiWidgetClass = window.SophiWebSDK?.SophiWidget;
if (!SophiWidgetClass) {
  throw new Error("Sophi SDK not loaded yet");
}

2) Initialize widget

const widget = new SophiWidgetClass();
await widget.init({
  apiKey: "YOUR_SOPHI_API_KEY",
  container: "#sophi-widget-container",
  environment: "production",
});

3) Full-page minimal example

import { useEffect, useRef } from "react";

declare global {
  interface Window {
    SophiWebSDK?: { SophiWidget: new () => any };
  }
}

type Cart = {
  cartId: string;
  items: Array<{ productId: string; variantId?: string; quantity: number }>;
};

export function SophiFullPage({ cart }: { cart: Cart }) {
  const widgetRef = useRef<any | null>(null);

  useEffect(() => {
    const SophiWidgetClass = window.SophiWebSDK?.SophiWidget;
    if (!SophiWidgetClass) return;

    const widget = new SophiWidgetClass();
    widgetRef.current = widget;

    (async () => {
      await widget.init({
        apiKey: "YOUR_SOPHI_API_KEY",
        container: "#sophi-widget-container",
        environment: "production",
        width: "100%",
        height: "100%",
      });

      widget.on("add_to_cart", async (data: { products: Array<{ productId: string; variantId?: string }> }) => {
        for (const p of data.products) {
          await addToCartInYourApp(p.productId, p.variantId);
        }
      });

      widget.on("send_to_checkout", () => {
        window.location.href = "/checkout";
      });

      widget.updateUserCart({
        cartId: cart.cartId,
        items: cart.items,
      });
    })();

    return () => {
      widgetRef.current?.destroy?.();
      widgetRef.current = null;
    };
  }, [cart.cartId, cart.items]);

  useEffect(() => {
    if (!widgetRef.current) return;
    widgetRef.current.updateUserCart({
      cartId: cart.cartId,
      items: cart.items,
    });
  }, [cart.cartId, cart.items]);

  return <div id="sophi-widget-container" style={{ width: "100%", height: "100vh" }} />;
}

4) Modal minimal example

import { useEffect, useRef, useState } from "react";

export function SophiModal({ cart }: { cart: { cartId: string; items: any[] } }) {
  const [open, setOpen] = useState(false);
  const widgetRef = useRef<any | null>(null);

  useEffect(() => {
    if (!open) return;
    const SophiWidgetClass = window.SophiWebSDK?.SophiWidget;
    if (!SophiWidgetClass) return;
    if (widgetRef.current) return;

    const widget = new SophiWidgetClass();
    widgetRef.current = widget;

    (async () => {
      await widget.init({
        apiKey: "YOUR_SOPHI_API_KEY",
        container: "#sophi-widget-modal-container",
        environment: "production",
        width: "100%",
        height: "100%",
      });

      widget.on("add_to_cart", async (data: { products: Array<{ productId: string; variantId?: string }> }) => {
        for (const p of data.products) {
          await addToCartInYourApp(p.productId, p.variantId);
        }
      });

      widget.on("send_to_checkout", () => {
        setOpen(false);
        window.location.href = "/checkout";
      });

      widget.updateUserCart({
        cartId: cart.cartId,
        items: cart.items,
      });
    })();

    return () => {
      widgetRef.current?.destroy?.();
      widgetRef.current = null;
    };
  }, [open, cart.cartId, cart.items]);

  useEffect(() => {
    if (!widgetRef.current) return;
    widgetRef.current.updateUserCart({
      cartId: cart.cartId,
      items: cart.items,
    });
  }, [cart.cartId, cart.items]);

  return (
    <>
      <button onClick={() => setOpen(true)}>Open assistant</button>
      {open && (
        <div role="dialog" aria-modal="true">
          <button onClick={() => setOpen(false)}>Close</button>
          <div id="sophi-widget-modal-container" style={{ width: "100%", height: "70vh" }} />
        </div>
      )}
    </>
  );
}

5) Required events and cart sync wiring

widget.on("add_to_cart", async (data) => {
  for (const product of data.products) {
    await addToCartInYourApp(product.productId, product.variantId);
  }
});

widget.on("send_to_checkout", () => {
  window.location.href = "/checkout";
});

widget.updateUserCart({
  cartId: cartId,
  items: cartItems,
});

Advanced setup

Call advanced methods only after ready event or widget.isReady():

widget.on("ready", () => {
  widget.sendToggleHistory();
  widget.triggerProductReply(product);
  widget.sendProductReply("Does this match my style?", product);
});
  • sendToggleHistory: open/close history panel
  • triggerProductReply: prefill product reply context
  • sendProductReply: send a typed product-context question

To make the assistant available before the visitor accepts cookies, initialize with storageConsent: false and upgrade in place when consent is granted. See Cookie Consent Management for the full concept.

The key points in React:

  1. Mount the widget once and pass the initial consent value via a ref so a later consent change does not destroy / re-init the widget.
  2. Watch the consent value and call widget.setStorageConsent(true) when it flips to granted.
import { useEffect, useRef } from "react";

export function SophiConsentAware({ hasConsent }: { hasConsent: boolean }) {
  const widgetRef = useRef<any | null>(null);
  // Read consent inside the init effect without re-running it on change.
  const hasConsentRef = useRef(hasConsent);
  useEffect(() => {
    hasConsentRef.current = hasConsent;
  }, [hasConsent]);

  // Mount the widget once — memory-only until consent.
  useEffect(() => {
    const SophiWidgetClass = window.SophiWebSDK?.SophiWidget;
    if (!SophiWidgetClass || widgetRef.current) return;

    const widget = new SophiWidgetClass();
    widgetRef.current = widget;

    void widget.init({
      apiKey: "YOUR_SOPHI_API_KEY",
      container: "#sophi-widget-container",
      environment: "production",
      width: "100%",
      height: "100%",
      storageConsent: hasConsentRef.current, // false → memory-only
    });

    return () => {
      widgetRef.current?.destroy?.();
      widgetRef.current = null;
    };
  }, []);

  // Upgrade the live widget in place when consent is granted.
  useEffect(() => {
    if (hasConsent) {
      void widgetRef.current?.setStorageConsent?.(true);
    }
  }, [hasConsent]);

  return <div id="sophi-widget-container" style={{ width: "100%", height: "100vh" }} />;
}

Security

  • Do not hardcode real keys in source code examples
  • Keep YOUR_SOPHI_API_KEY as placeholder in docs
  • Keep private values out of GTM/static code

On this page