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
2) Add a container element
Mount a target div in your JSX. The SDK will inject the iframe here.
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:
The same hook and event pattern shown above applies.