Three Ways to Integrate
React and Next.js apps have different constraints than traditional websites. Script tags need lifecycle management. Single-page navigation means the widget must persist across route changes. And if your app has authentication, you want the agent to know who the user is.
Here are three integration approaches, from simplest to most feature-complete.
Approach 1: Script Tag with useEffect (Simplest)
The fastest way to get an agent into your React app is the same script tag used on any website, loaded via useEffect.
import { useEffect } from 'react';
function AgentWidget() {
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://hiroi.ai/widget.js';
script.setAttribute('data-site-id', 'your-site-uuid-here');
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
return null;
}
export default AgentWidget;
Drop this component into your app's root layout and the agent appears on every page.
When to use this: Prototyping, simple apps, or when you want the agent on every route without any user-specific context.
Placement in Your App
Add the component at the top level so it persists across navigation:
// App.tsx or layout component
function App() {
return (
<>
<Router>
<Routes>
{/* your routes */}
</Routes>
</Router>
<AgentWidget />
</>
);
}
Approach 2: React Component Wrapper
For more control, wrap the widget in a proper React component with props and cleanup.
import { useEffect, useRef } from 'react';
interface HiroiChatProps {
siteId?: string;
sessionToken?: string;
}
function HiroiChat({ siteId, sessionToken }: HiroiChatProps) {
const loaded = useRef(false);
useEffect(() => {
if (loaded.current) return;
loaded.current = true;
const script = document.createElement('script');
script.src = 'https://hiroi.ai/widget.js';
script.async = true;
if (sessionToken) {
script.setAttribute('data-session-token', sessionToken);
} else if (siteId) {
script.setAttribute('data-site-id', siteId);
}
document.body.appendChild(script);
return () => {
// Clean up widget DOM elements on unmount
const container = document.querySelector('.va-container');
if (container) container.remove();
document.body.removeChild(script);
loaded.current = false;
};
}, [siteId, sessionToken]);
return null;
}
export default HiroiChat;
Usage:
// Public pages - domain safelist auth
<HiroiChat siteId="your-site-uuid" />
// Authenticated pages - session token auth
<HiroiChat sessionToken={userSessionToken} />
The loaded ref prevents double-initialization in React 18's StrictMode, which runs effects twice in development.
Approach 3: Next.js Script Component
Next.js provides a Script component with built-in loading strategies. This is the recommended approach for Next.js apps.
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://hiroi.ai/widget.js"
data-site-id="your-site-uuid-here"
strategy="afterInteractive"
/>
</body>
</html>
);
}
The afterInteractive strategy loads the script after the page becomes interactive, which is the right timing for an agent. It should not delay your initial page load, but it should be ready shortly after the user can interact with your app.
App Router vs Pages Router
App Router (Next.js 13+): Place the Script component in your root layout.tsx as shown above. It automatically persists across all routes.
Pages Router: Add it to _app.tsx:
import Script from 'next/script';
import type { AppProps } from 'next/app';
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script
src="https://hiroi.ai/widget.js"
data-site-id="your-site-uuid-here"
strategy="afterInteractive"
/>
</>
);
}
Session-Signed Auth for Authenticated Apps
If your app has user authentication, you want the agent to know who is using it. Session-signed auth lets your backend create a secure token that the widget uses instead of a site ID.
Backend: Create the Token
On your server (API route, Express endpoint, or serverless function), call the hiroi session API:
// Next.js API route: /api/chat-token
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const { userId, userName } = await request.json();
const response = await fetch(
'https://hiroi.ai/api/widget/session/create',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Server-Key': process.env.HIROI_SERVER_SECRET!, // ss_... key
},
body: JSON.stringify({
user_id: userId,
user_name: userName,
ttl_minutes: 30,
}),
}
);
const { session_token } = await response.json();
return NextResponse.json({ sessionToken: session_token });
}
The X-Server-Key is your server secret (starts with ss_). It is never exposed to the browser. Store it in your environment variables.
Frontend: Fetch Token and Pass to Widget
'use client';
import { useEffect, useState } from 'react';
import { useSession } from 'next-auth/react'; // or your auth library
function AuthenticatedChat() {
const { data: session } = useSession();
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
if (!session?.user) return;
fetch('/api/chat-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: session.user.id,
userName: session.user.name,
}),
})
.then((res) => res.json())
.then((data) => setToken(data.sessionToken));
}, [session?.user]);
if (!token) return null;
return <HiroiChat sessionToken={token} />;
}
This flow ensures:
- The server secret never reaches the browser
- Each session token is tied to a specific user and has a configurable expiry
- Conversations in your hiroi dashboard show the user's identity
- Tokens are HMAC-SHA256 signed and site-specific, so they cannot be reused across different agents
TypeScript Considerations
The widget script adds elements to the DOM dynamically. If you need to interact with it programmatically, declare the types:
// types/hiroi.d.ts
declare global {
interface Window {
VAWidget?: {
destroy: () => void;
// Add other methods as needed
};
}
}
export {};
For the Next.js Script component, the data-site-id and data-session-token attributes are not in the default type definitions. You can extend them or use a type assertion if TypeScript complains.
Handling SPA Navigation
Single-page apps navigate without full page reloads. The agent widget handles this naturally when placed at the root layout level because it is mounted once and persists across route changes.
However, there are scenarios to be aware of:
Conditional Rendering
If you only want the agent on certain routes, conditionally render the component:
import { usePathname } from 'next/navigation';
function ConditionalChat() {
const pathname = usePathname();
const showChat = !pathname.startsWith('/admin')
&& !pathname.startsWith('/checkout');
if (!showChat) return null;
return <HiroiChat siteId="your-site-uuid" />;
}
Route-Specific Context
If your agent uses page integration and you want the AI to be aware of route changes, the widget automatically detects DOM changes for configured page fields. When a user navigates from one product page to another, the AI context updates without any additional code on your part.
Quick Reference
| Scenario | Approach | Auth Method |
|---|---|---|
| Public marketing site | Script tag or Next.js Script | Domain safelist |
| App with no auth | useEffect wrapper | Domain safelist |
| App with user auth | Component wrapper + API route | Session-signed |
| Admin panel or internal tool | Conditional render + session token | Session-signed |
Start with the simplest approach that meets your needs. You can always upgrade from a script tag to session-signed auth later without changing the user experience. The widget looks and behaves identically regardless of the authentication method behind it.