Skip to main content
How to Add an AI Chatbot to a React or Next.js Site

How to Add an AI Chatbot to a React or Next.js Site

Embed an AI chatbot in a React or Next.js app the right way: load the widget scripts safely on the client, avoid SSR crashes, and clean up on unmount.

Jul 24, 20267 min readAskvo Team

The Askvo widget is a plain custom element (<chatbot-widget>) plus two script tags, no React wrapper needed, no package to install. But "just paste the snippet" glosses over two things that actually break in a React or Next.js app if you don't handle them: server-side rendering has no window or document, and unmounted components need to clean up after themselves. This guide covers both, plus training the bot on the site's content.

Step 1: Train the bot on your content

  1. Create a free Askvo account. The Free plan includes one chatbot, no card required.
  2. Paste your deployed site's URL. The crawler needs a live, publicly reachable URL and can't read a localhost dev server. If you haven't deployed yet, a preview deployment (Vercel, Netlify, etc.) works fine.
  3. Fill in what the crawler can't see. Content rendered only after a user interaction, gated docs, pricing in a modal. Upload documents or add Q&A pairs directly; see how to train a chatbot on your own data.
  4. Test it in the dashboard before shipping. In strict mode it only answers from your content and says so when it doesn't know something.

Step 2: Get your embed snippet

Copy the snippet from your bot's install page in the Askvo dashboard. The two script URLs and the chatbot ID are what you'll actually need below; the rest are just the custom element's attributes.

Step 3a: Next.js — use the built-in Script component

Next.js's <Script> component handles load timing and deduping for you. Add it once, in your root layout, so it's on every route:

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://widget.yourdomain.example/polyfills.js" type="module" strategy="afterInteractive" />
        <Script src="https://widget.yourdomain.example/main.js" type="module" strategy="afterInteractive" />
        <chatbot-widget
          chatbot-id="YOUR_CHATBOT_ID"
          api-url="https://yourdomain.example"
          primary-color="#your-color"
          welcome-message="Hi! How can I help?"
          show-branding="true"
        />
      </body>
    </html>
  );
}

strategy="afterInteractive" loads the scripts after the page is interactive, which is what you want for a widget that isn't part of the critical render path. The custom element itself renders fine directly in JSX. React passes through unknown lowercase tags with dashes as native custom elements without complaint.

Step 3b: Plain React (Vite, CRA, etc.): load scripts in an effect

Without Next.js's Script component, load the scripts yourself in a useEffect so they only run in the browser, and guard against double-loading in strict mode's double-invoke:

import { useEffect } from 'react';

function useChatWidget() {
  useEffect(() => {
    if (document.querySelector('chatbot-widget')) return; // already mounted

    const polyfills = document.createElement('script');
    polyfills.src = 'https://widget.yourdomain.example/polyfills.js';
    polyfills.type = 'module';
    document.body.appendChild(polyfills);

    const main = document.createElement('script');
    main.src = 'https://widget.yourdomain.example/main.js';
    main.type = 'module';
    document.body.appendChild(main);

    const widget = document.createElement('chatbot-widget');
    widget.setAttribute('chatbot-id', 'YOUR_CHATBOT_ID');
    widget.setAttribute('api-url', 'https://yourdomain.example');
    widget.setAttribute('primary-color', '#your-color');
    widget.setAttribute('welcome-message', 'Hi! How can I help?');
    widget.setAttribute('show-branding', 'true');
    document.body.appendChild(widget);

    return () => {
      widget.remove();
      // Leave the two script tags in place — removing and re-adding
      // ES modules on every unmount/remount re-triggers their side
      // effects unnecessarily; the widget element itself is what
      // actually needs to come and go.
    };
  }, []);
}

Call useChatWidget() once near the top of your app (a root layout component, or App.tsx) so it mounts once for the whole session rather than per-route.

Why the SSR guard and cleanup actually matter

Skip the useEffect/afterInteractive pattern and reach straight for window or document at the top of a component, and a Next.js server render throws immediately, since there's no DOM on the server. Skip the cleanup function, and React's fast-refresh or route-level remounts during development can leave duplicate widget elements in the DOM. Neither is Askvo-specific; it's the same pattern any third-party DOM-manipulating script needs in a React app.

Step 4: Verify before you call it done

  • Check the browser console for hydration warnings after adding the widget — there shouldn't be any if it's mounted client-side only.
  • Navigate between routes and confirm the widget persists rather than disappearing or duplicating.
  • Ask it a real question and confirm the reply streams in with a citation.
  • Test "talk to a human" and confirm the conversation reaches your Askvo dashboard.

Keeping answers accurate as your site changes

Re-crawl your deployed URL after shipping meaningful content changes. Check the Unanswered Questions report to catch what's missing. Compare plans and message-credit limits on pricing.

Frequently asked questions

Why does the widget break my Next.js server render?

Only if you skip next/script's strategy prop or reach for window/document outside a client-only boundary. Following the pattern above avoids it entirely.

Does React complain about an unknown custom element in JSX?

No. React 17+ passes through elements with a hyphen in the tag name as native custom elements and forwards attributes as-is, rather than treating them as unrecognized DOM props.

Can I train it against localhost during development?

No. The crawler needs a publicly reachable URL. Use a preview deployment or your production URL, then re-crawl after real content changes.

What happens if the bot doesn't know an answer?

It says so with a fallback message you control, and logs the question in your Unanswered Questions report.

Try it on your own React site

Train a free chatbot on your content, then paste the snippet — free, no credit card.

Get Started Free

Other platforms

All integrations