Skip to content

React integration guide

This guide covers patterns for integrating the Univerx Client SDK into React applications, including a reusable custom hook and Next.js-specific considerations.

Create the SDK instance inside useEffect so it only runs in the browser. Always call widget.destroy() in the cleanup function to prevent memory leaks and stale connections.

import { WidgetClient } from "@univerx/client-sdk";
import { useEffect, useRef } from "react";
export function SupportWidget() {
const widgetRef = useRef<WidgetClient | null>(null);
useEffect(() => {
const widget = new WidgetClient({
widgetKey: "your-widget-key",
onAgentAssigned: (agent) => console.log(`Agent: ${agent.name}`),
onCallEnded: () => widgetRef.current?.destroy(),
});
widget.init();
widgetRef.current = widget;
return () => {
widgetRef.current?.destroy();
};
}, []);
// The SDK renders its own UI — this component is a lifecycle wrapper
return null;
}

Extract the widget logic into a reusable hook to keep components clean and expose widget state to your UI:

import { WidgetClient, WidgetConfig, WidgetState } from "@univerx/client-sdk";
import { useEffect, useRef, useState } from "react";
export function useWidget(config: WidgetConfig) {
const widgetRef = useRef<WidgetClient | null>(null);
const [state, setState] = useState<WidgetState>("idle");
const [queuePosition, setQueuePosition] = useState<number | null>(null);
useEffect(() => {
const widget = new WidgetClient({
...config,
onReady: () => setState("ready"),
onCallStarted: () => setState("in_call"),
onCallEnded: () => setState("ended"),
});
widget.on("queue:update", (position) => setQueuePosition(position));
widget.on("queue:left", () => setQueuePosition(null));
widget.init();
widgetRef.current = widget;
return () => {
widget.destroy();
widgetRef.current = null;
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return {
widget: widgetRef.current,
state,
queuePosition,
};
}

Usage in a component:

export function SupportButton() {
const { widget, state, queuePosition } = useWidget({
widgetKey: "your-widget-key",
});
const handleClick = async () => {
if (state === "ready") {
await widget?.joinQueue({ terms: true });
}
};
return (
<button onClick={handleClick} disabled={state !== "ready"}>
{state === "queued" ? `Position ${queuePosition}` : "Get support"}
</button>
);
}

When an agent requests cobrowsing, the SDK emits cobrowse:consent:required with handler functions. Render your own consent UI and call the appropriate handler:

useEffect(() => {
const widget = new WidgetClient({ widgetKey: "your-widget-key" });
widget.on("cobrowse:consent:required", ({ accept, decline }) => {
// Show your consent modal, then call accept() or decline()
setConsentHandlers({ accept, decline });
setShowConsentModal(true);
});
widget.init();
widgetRef.current = widget;
return () => widget.destroy();
}, []);

Add "use client" at the top of any file that imports or instantiates the SDK:

"use client";
import { WidgetClient } from "@univerx/client-sdk";

In Next.js, environment variables accessed on the client must use the NEXT_PUBLIC_ prefix:

Terminal window
NEXT_PUBLIC_UNIVERX_WIDGET_KEY=your-widget-key-here
const widget = new WidgetClient({
widgetKey: process.env.NEXT_PUBLIC_UNIVERX_WIDGET_KEY!,
});

To render the widget on every page, add it to your root layout. Because SupportWidget is a client component, the layout itself can remain a server component:

app/layout.tsx
import { SupportWidget } from "@/components/SupportWidget";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<SupportWidget />
</body>
</html>
);
}

For the Pages Router, add the widget to _app.tsx:

pages/_app.tsx
import type { AppProps } from "next/app";
import { SupportWidget } from "@/components/SupportWidget";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<SupportWidget />
</>
);
}