Adding Playbott to a React Application
Complete guide to integrating the Playbott AI chat widget into React and Next.js applications with dynamic user authentication, live theme switching, and SPA route synchronization.
Adding Playbott to a React Application
Integrating the Playbott AI chat widget into modern Single Page Applications (SPAs) like React, Vite, Next.js, or Remix requires more than just pasting a static <script> tag. To deliver a seamless user experience, your widget should dynamically synchronize with:
- User Authentication State: Surface logged-in user details (name, email, role, phone) directly to AI triage and live support agents.
- Dynamic Theme Switching: Instantly mirror your application's light or dark mode in real time without refreshing the page.
- Client-Side Routing: Notify the widget of path changes so URL visibility rules (e.g., hiding on
/checkoutor/admin) evaluate accurately during client-side navigation.
This guide follows the exact production architecture used by Playbott's own dashboard and public portal to manage the chat widget lifecycle.
Prerequisites
Before building your widget loader component:
- Active Playbott Account: Sign in to your Playbott Dashboard.
- Website Public Key: Go to Organisation Settings → Websites and copy your Website Public Key (
data-key). - Widget Script URL: Note your CDN script endpoint (typically
https://playbott.com/widget.jsor your custom domain).
Step 1: Configure Environment Variables
Define your widget configuration in your project's .env file:
# Vite (.env)
VITE_WIDGET_SCRIPT_URL=https://playbott.com/widget.js
VITE_WIDGET_DATA_KEY=pk_live_your_website_public_key_here
# Next.js (.env.local)
NEXT_PUBLIC_WIDGET_SCRIPT_URL=https://playbott.com/widget.js
NEXT_PUBLIC_WIDGET_DATA_KEY=pk_live_your_website_public_key_here
Step 2: Define TypeScript Types
Create a type definition for user identification data. This ensures full type safety across your authentication context and the widget API:
// types/widget.ts
export interface WidgetAuthInfo {
external_user_id?: string;
user_name?: string;
user_email?: string;
user_phone?: string;
user_role?: string;
}
declare global {
interface Window {
ChatWidget?: {
setAuthInfo: (info: WidgetAuthInfo | null) => void;
setDarkMode: (isDark: boolean) => void;
open: () => void;
close: () => void;
toggle: () => void;
};
}
}
Step 3: Create the WidgetLoader Component
Create a renderless lifecycle component WidgetLoader.tsx that manages the widget script:
// components/WidgetLoader.tsx
import { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom"; // Or useRouter in Next.js
import type { WidgetAuthInfo } from "@/types/widget";
import { useAuth } from "@/hooks/useAuth"; // Your auth hook
import { useTheme } from "@/hooks/useTheme"; // Your theme hook
const WIDGET_SCRIPT_URL = import.meta.env.VITE_WIDGET_SCRIPT_URL?.trim() ?? "";
const WIDGET_DATA_KEY = import.meta.env.VITE_WIDGET_DATA_KEY?.trim() ?? "";
function buildAuthInfo(user: any): WidgetAuthInfo | null {
if (!user) return null;
const email = typeof user.email === "string" ? user.email.trim() : "";
const phone = user.phone || user.mobile || "";
if (!email && !phone && !user.id) return null;
return {
external_user_id: user.id || user._id,
user_name: user.name || user.fullName || undefined,
user_email: email || undefined,
user_phone: phone || undefined,
user_role: user.role || user.activeRole || undefined,
};
}
export function WidgetLoader() {
const { user } = useAuth();
const { isDark } = useTheme();
const location = useLocation();
const isScriptInjected = useRef(false);
// 1. Initial Injection (Injects exactly once on application mount)
useEffect(() => {
if (!WIDGET_SCRIPT_URL || !WIDGET_DATA_KEY) {
return;
}
if (
isScriptInjected.current ||
document.getElementById("chat-widget-container")
) {
return;
}
const rootIsDark = document.documentElement.classList.contains("dark");
const script = document.createElement("script");
script.src = WIDGET_SCRIPT_URL;
script.setAttribute("data-key", WIDGET_DATA_KEY);
script.setAttribute(
"data-auth-info",
JSON.stringify(buildAuthInfo(user) ?? {})
);
script.setAttribute("data-dark-mode", String(rootIsDark));
script.async = true;
script.onload = () => {
window.ChatWidget?.setDarkMode(rootIsDark);
};
document.body.appendChild(script);
isScriptInjected.current = true;
}, []);
// 2. Dynamic User Session Sync (Updates on login / logout without DOM teardown)
useEffect(() => {
if (!WIDGET_SCRIPT_URL || !WIDGET_DATA_KEY) return;
window.ChatWidget?.setAuthInfo(buildAuthInfo(user));
}, [user?.id, user?.email]);
// 3. Dynamic Dark / Light Theme Sync
useEffect(() => {
if (!WIDGET_SCRIPT_URL || !WIDGET_DATA_KEY) return;
window.ChatWidget?.setDarkMode(isDark);
}, [isDark]);
// 4. SPA Route Path Sync (Synchronizes URL changes with widget visibility rules)
useEffect(() => {
if (!WIDGET_SCRIPT_URL || !WIDGET_DATA_KEY) return;
const iframe = document.querySelector<HTMLIFrameElement>(
"#chat-widget-container iframe"
);
if (iframe && iframe.contentWindow) {
iframe.contentWindow.postMessage(
{
type: "chatwidget:path-update",
path: location.pathname,
fullUrl: window.location.href,
hash: location.hash,
},
"*"
);
}
}, [location.pathname, location.search, location.hash]);
return null;
}
export default WidgetLoader;
Step 4: Mount the Loader in Your Application Root
Mount <WidgetLoader /> at the root of your component tree (inside your router and state providers) so it remains active across all page navigations:
// App.tsx
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "@/context/AuthContext";
import { ThemeProvider } from "@/context/ThemeContext";
import { WidgetLoader } from "@/components/WidgetLoader";
import { AppRoutes } from "@/routes/AppRoutes";
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<ThemeProvider>
<AppRoutes />
{/* Mount WidgetLoader alongside global modals & toast notifications */}
<WidgetLoader />
</ThemeProvider>
</AuthProvider>
</BrowserRouter>
);
}
Next.js (App Router) Integration
If you are using Next.js 14+ with the App Router, mark the loader as a client component ("use client"):
// app/components/WidgetLoader.tsx
"use client";
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import { useTheme } from "next-themes";
import { useSession } from "next-auth/react";
export function WidgetLoader() {
const pathname = usePathname();
const { resolvedTheme } = useTheme();
const { data: session } = useSession();
const isScriptInjected = useRef(false);
useEffect(() => {
if (isScriptInjected.current) return;
const script = document.createElement("script");
script.src = process.env.NEXT_PUBLIC_WIDGET_SCRIPT_URL || "";
script.setAttribute("data-key", process.env.NEXT_PUBLIC_WIDGET_DATA_KEY || "");
script.async = true;
document.body.appendChild(script);
isScriptInjected.current = true;
}, []);
useEffect(() => {
window.ChatWidget?.setDarkMode(resolvedTheme === "dark");
}, [resolvedTheme]);
useEffect(() => {
if (session?.user) {
window.ChatWidget?.setAuthInfo({
external_user_id: session.user.id,
user_name: session.user.name || undefined,
user_email: session.user.email || undefined,
});
} else {
window.ChatWidget?.setAuthInfo(null);
}
}, [session]);
return null;
}
Mount it inside your Root Layout (app/layout.tsx):
// app/layout.tsx
import { WidgetLoader } from "./components/WidgetLoader";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<WidgetLoader />
</body>
</html>
);
}
Programmatic API Control
The Playbott widget exposes programmatic methods on window.ChatWidget that you can call from custom buttons or support links:
// Open the chat window
window.ChatWidget?.open();
// Close the chat window
window.ChatWidget?.close();
// Toggle chat window open / closed
window.ChatWidget?.toggle();
// Update user identification on the fly
window.ChatWidget?.setAuthInfo({
user_name: "Sarah Connor",
user_email: "sarah@cyberdyne.io",
user_role: "Account Admin",
});
// Force dark mode state
window.ChatWidget?.setDarkMode(true);
Example: Custom "Chat with Support" Button
import { MessageSquare } from "lucide-react";
export function SupportButton() {
return (
<button
type="button"
onClick={() => window.ChatWidget?.open()}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm shadow-sm hover:opacity-90 transition-all cursor-pointer"
>
<MessageSquare className="w-4 h-4" />
<span>Chat with Support</span>
</button>
);
}
Best Practices & Security
Do not re-inject the script on re-renders: Always use a useRef(false) guard or check for an existing #chat-widget-container before creating a new <script> element. Re-injecting the script can create duplicate WebSocket connections and widget bubble instances.
Content Security Policy (CSP): If your React application enforces strict CSP headers, ensure you allow Playbott CDN and WebSocket endpoints:
script-src https://playbott.com; frame-src https://playbott.com; connect-src https://playbott.com wss://playbott.com;Frequently Asked Questions
Why should I use a React component instead of a raw script tag in index.html?
Using a dedicated React component (like WidgetLoader) enables dynamic user identification upon login/logout, instant dark/light theme switching without page reloads, and route synchronization for SPA URL visibility rules.
Does the Playbott widget support Server-Side Rendering (SSR) in Next.js or Remix?
Yes. The widget loader script runs strictly on the client side inside useEffect or as a Next.js client component ('use client') with zero server-side hydration mismatches.
How does the widget handle user login and logout in single-page apps?
You do not need to reload the page or re-inject the script. Simply call window.ChatWidget.setAuthInfo(userData) on login and window.ChatWidget.setAuthInfo(null) on logout to seamlessly update the chat session.
How do URL visibility rules work with client-side React Router?
The WidgetLoader component posts a 'chatwidget:path-update' message to the widget iframe whenever location.pathname changes, allowing Playbott to show or hide the widget automatically based on your dashboard rules.
Ready to deploy Playbott on your website?
Get started in minutes with our embedded AI chat widget and omnichannel ticketing.