Skip to content

React Integration

This guide shows a safe React integration pattern using a custom hook to own the widget lifecycle:

  • Isolate widget state inside a hook
  • Initialise in a user action (not useEffect) to avoid double-initialisation in Strict Mode
  • Handle events in application code
  • Destroy the widget on unmount

1) Install the SDK

npm install @usesophi/sophi-web-sdk

2) Add a container element

Mount a target div in your JSX. The SDK will inject the iframe here.

<div id="sophi-widget"></div>

3) Custom hook

Encapsulate all widget logic in a hook so your components stay clean.

import { useEffect, useRef, useState, useCallback } from "react";
import {
  SophiWidget,
  type UserData,
  type AddToCartEvent,
  type AddToFavoriteEvent,
  type ProductSummary,
  type ProductReplyOptions,
  type OutfitResponse,
} from "@usesophi/sophi-web-sdk";

export function useSophiWidget() {
  const widgetRef = useRef<SophiWidget | null>(null);
  const [isInitialized, setIsInitialized] = useState(false);
  const [isLoggedIn, setIsLoggedIn]       = useState(false);
  const [isVisible, setIsVisible]         = useState(true);

  const initWidget = useCallback(async (apiKey: string, userId?: string) => {
    // Destroy any existing instance before re-initialising (login/logout flow)
    if (widgetRef.current) {
      await widgetRef.current.destroy();
      widgetRef.current = null;
    }

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

    await widget.init({
      apiKey,
      container: "#sophi-widget",
      userId,           // omit or pass undefined for a guest session
      environment: "production",
      width: "100%",
      height: "600px",
      onReady: () => {
        setIsInitialized(true);
        setIsLoggedIn(!!userId);
        setIsVisible(true);
      },
      onError: (error) => console.error("Sophi init error", error),
    });

    widget.on("add_to_cart", (data: AddToCartEvent) => {
      data.products.forEach((p) => addToCartInYourApp(p));
    });

    widget.on("add_to_favorite", (data: AddToFavoriteEvent) => {
      addToWishlistInYourApp(data.productIdentifier);
    });

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

    widget.on("error", (error: Error) => {
      console.error("Sophi widget error", error);
    });
  }, []);

  // Login: re-init with a real userId — SDK merges the guest session automatically
  const loginUser = useCallback(
    (apiKey: string, userId: string) => initWidget(apiKey, userId),
    [initWidget],
  );

  // Logout: re-init without userId — SDK creates a fresh guest session
  const logoutUser = useCallback(
    (apiKey: string) => initWidget(apiKey, undefined),
    [initWidget],
  );

  const toggleWidget = useCallback(() => {
    if (!widgetRef.current) return;
    if (isVisible) { widgetRef.current.hide(); setIsVisible(false); }
    else           { widgetRef.current.show(); setIsVisible(true);  }
  }, [isVisible]);

  const destroyWidget = useCallback(async () => {
    await widgetRef.current?.destroy();
    widgetRef.current = null;
    setIsInitialized(false);
  }, []);

  // Chat message helpers
  const sendTextMessage = useCallback(
    (query: string) => widgetRef.current?.sendTextMessage(query),
    [],
  );

  const sendProductReply = useCallback(
    (query: string, product: ProductSummary, opts?: ProductReplyOptions) =>
      widgetRef.current?.sendProductReply(query, product, opts),
    [],
  );

  const sendOutfitReply = useCallback(
    (query: string, outfit: OutfitResponse) =>
      widgetRef.current?.sendOutfitReply(query, outfit),
    [],
  );

  // Cleanup on unmount
  useEffect(() => {
    return () => { widgetRef.current?.destroy(); };
  }, []);

  return {
    isInitialized,
    isLoggedIn,
    isVisible,
    initWidget,
    loginUser,
    logoutUser,
    toggleWidget,
    destroyWidget,
    sendTextMessage,
    sendProductReply,
    sendOutfitReply,
  };
}

4) Component example

import { useSophiWidget } from "./hooks/useSophiWidget";
import type { ProductSummary, OutfitResponse } from "@usesophi/sophi-web-sdk";

export function ShopPage() {
  const { isInitialized, initWidget, sendTextMessage, sendProductReply, sendOutfitReply } =
    useSophiWidget();

  return (
    <>
      {/* Widget renders here */}
      <div id="sophi-widget" style={{ width: "100%", height: "600px" }} />

      <button onClick={() => initWidget("YOUR_SOPHI_API_KEY")}>
        Open Sophi
      </button>

      {isInitialized && (
        <>
          <button onClick={() => sendTextMessage("What should I wear today?")}>
            Ask Sophi
          </button>

          {/* Call when the user taps "Ask about this" on a product card */}
          <button onClick={() => sendProductReply("Does this come in blue?", product)}>
            Ask about product
          </button>

          {/* Pass outfit context when the product is part of a recommendation */}
          <button
            onClick={() =>
              sendProductReply("Tell me about this jacket", product, {
                outfitId: 7,
                isOutfit: true,
              })
            }
          >
            Ask about outfit product
          </button>

          {/* Call when the user taps "Ask about this outfit" */}
          <button onClick={() => sendOutfitReply("Can you style this differently?", outfit)}>
            Ask about outfit
          </button>
        </>
      )}
    </>
  );
}

5) Auth flow — guest → login → logout

The SDK manages session merging automatically. Re-initialise with the correct userId when auth state changes; the SDK handles the rest.

Scenario What to pass SDK behaviour
Guest session userId omitted or undefined Creates an anonymous session
Login userId: "real-user-id" Merges guest session into the logged-in user
Logout userId omitted or undefined Clears the session and creates a fresh guest session
// Guest → logged-in
await initWidget(apiKey);                     // guest
await loginUser(apiKey, "user-42");           // merges guest session

// Log out
await logoutUser(apiKey);                     // fresh guest session

Warning

Avoid calling initWidget inside a useEffect with an empty dependency array — React Strict Mode runs effects twice in development and will produce a double-init warning. Trigger initialisation from a user action or a consent callback instead.

6) If using the browser global (no npm)

If you load the SDK via a <script> tag or GTM, instantiate through window.SophiWebSDK:

const widget = new window.SophiWebSDK.SophiWidget();

The same hook and event pattern shown above applies.