Getting Started

Integrate hiroi AI Chat into React and Next.js Apps

How to embed an AI agent in React and Next.js apps using script tags, component wrappers, and session-signed auth for logged-in users.

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.

Try hiroi free.

Deploy an AI agent across chat, voice, email, and SMS — no credit card required.