Add an AI Chatbot to a React App
A practical guide to adding an AI chatbot to a React app—script tag vs. component, hydration gotchas, lazy loading, events, and lead capture.
There is a specific kind of pain that comes from dropping a third-party widget into a React app and watching your console light up red. You pasted a script tag that worked perfectly on a plain HTML page, and now React is yelling about hydration mismatches, the widget mounts twice in development, or it vanishes the moment you navigate between routes. Adding an AI chatbot for React is not hard, but it is different from adding one to a static site, because React owns the DOM and a naive script injection fights that ownership. This guide walks through the right way to add a react chatbot widget—the patterns that survive client-side routing, Strict Mode, server-side rendering, and code splitting—using concrete code you can paste and adapt.
We will cover the two real integration styles (a global embed script versus a controlled React component), the gotchas unique to React's lifecycle, how to lazy-load the widget so it never blocks your Largest Contentful Paint, and how to wire chatbot events into your own analytics and lead flow. The examples assume a typical setup—Create React App, Vite, or Next.js with the App Router—and they apply whether your bot comes from Alee, Intercom, Crisp, or a custom build.
Why an AI chatbot for React needs a different approach
On a hand-coded HTML page, you drop a <script> before </body>, the vendor's loader appends a chat bubble to document.body, and you are done. React breaks two assumptions that pattern relies on.
First, React controls a subtree of the DOM and re-renders it. Most chat widgets are smart enough to mount themselves on document.body, outside your React root, so React's reconciler never touches them. That is usually fine. The trouble starts when developers try to render the widget's container inside a component's JSX—then React's virtual DOM and the widget's imperative DOM manipulation collide, and you get flicker, double mounts, or nodes that get ripped out on the next render.
Second, most React apps are single-page apps with client-side routing. The page never does a full reload as users move between views. A script that only runs once on initial page load may not re-trigger the logic a widget expects on "page change," which matters for analytics attribution and for context-aware bots that read the current URL.
There is also Strict Mode. In React 18 and 19 development builds, Strict Mode intentionally mounts, unmounts, and remounts every component once to surface side-effect bugs. If your widget-loading useEffect is not idempotent, you will load the script twice and possibly render two chat bubbles. This only happens in development, but it is the single most common "why are there two chatbots" question, so we will handle it explicitly.
If you want the conceptual background on how a content-trained bot actually answers questions before you wire one in, the primer on what RAG is explains retrieval-augmented generation in plain terms—useful context for why these bots feel different from scripted FAQ widgets.
Two ways to add a react chatbot widget
There are exactly two integration shapes, and choosing the right one upfront saves rework.
Option A: the global embed script
The vendor gives you a snippet like this:
- A
<script>that loads a small loader file from their CDN - A site or workspace ID that tells the loader which bot to render
- Optional config (color, position, greeting) passed via a global config object
This is the right default for most apps. The widget lives outside your React tree on document.body, so it is immune to your re-renders, and it works identically across every route without you lifting a finger. The only real work is loading the script once, cleanly, in a React-aware way.
Option B: a React component / npm package
Some vendors ship an official React package (npm install @vendor/react-chat) that exposes a <ChatWidget /> component and hooks. This gives you tighter control: you can pass props reactively, call methods imperatively via refs, and let React's lifecycle manage mount and unmount.
Use Option B when:
- You need to show or hide the widget conditionally (for example, only for logged-in users, or only on support pages).
- You want to pass dynamic data into the bot—the current user's name, plan tier, or the product they are viewing—and have it update when that data changes.
- You are deep in a TypeScript codebase and want typed props and events instead of stringly-typed global config.
If no official package exists, do not wrap the embed script in a half-baked component just for aesthetics. A clean script loader (Option A) is more robust than a leaky abstraction. With a platform like Alee, you can start with the embed script today and migrate to a component later without retraining the bot, because the bot's knowledge lives server-side, independent of how you mount the UI.
Option A in practice: load the embed script the React way
The goal is to inject the vendor script exactly once, after your app is interactive, and to never block initial render. Here is a reusable hook.
```jsx
// useChatWidget.js
import { useEffect } from "react";
export function useChatWidget({ src, widgetId }) {
useEffect(() => {
// Idempotency guard: survive Strict Mode double-invoke
// and client-side route changes.
if (document.getElementById("chat-widget-script")) return;
const script = document.createElement("script");
script.id = "chat-widget-script";
script.src = src;
script.async = true;
script.dataset.widgetId = widgetId;
document.body.appendChild(script);
// No cleanup that removes the script: the widget mounts on
// document.body and should persist across route changes.
}, [src, widgetId]);
}
```
Then call it once near the root of your app:
```jsx
// App.jsx
function App() {
useChatWidget({
src: "https://cdn.aleeup.com/widget.js",
widgetId: "your-widget-id",
});
return <Router>{/ your routes /}</Router>;
}
```
A few deliberate choices here are worth calling out:
- The `getElementById` guard is the whole trick. It makes the effect idempotent. Strict Mode can run the effect twice; client-side navigation can remount
Appin some setups; this guard means the script is appended exactly once regardless. No double bubbles. - `async` is set so the script never blocks parsing or your main bundle's execution.
- We deliberately do not remove the script in cleanup. Chat widgets keep state—open conversations, unread counts—and tearing them down on every unmount is hostile to the user. Load once, leave it.
- We mount the hook at the root, not inside a route, so the widget is available everywhere and is not re-initialized as users navigate.
Telling the widget about route changes
Because your SPA does not reload between routes, a context-aware bot needs a nudge when the URL changes. Most vendors expose a global method for this. With React Router:
```jsx
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
function ChatRouteSync() {
const location = useLocation();
useEffect(() => {
// Call only if the widget API is loaded.
if (window.AleeWidget?.pageChanged) {
window.AleeWidget.pageChanged(location.pathname);
}
}, [location.pathname]);
return null;
}
```
Drop <ChatRouteSync /> inside your <Router>. Now every client-side navigation tells the bot which page the visitor is on, so its answers and your analytics stay accurate. Check your vendor's docs for the exact method name—the pattern is identical, only the API call differs.
Option B in practice: the component approach
When a vendor ships a real package, the integration is cleaner and more idiomatic:
```jsx
import { ChatWidget } from "@vendor/react-chat";
function App({ currentUser }) {
return (
<>
{/ your app /}
<ChatWidget
widgetId="your-widget-id"
user={
currentUser
? { name: currentUser.name, email: currentUser.email }
: undefined
}
onLeadCaptured={(lead) => {
// forward to your CRM / analytics
track("chatbot_lead", lead);
}}
/>
</>
);
}
```
The advantages are real: when currentUser changes, the widget updates reactively; you get a typed onLeadCaptured callback instead of subscribing to a global event bus; and React unmounts it cleanly if you conditionally render it.
If you go this route, still keep two rules in mind:
- Render the component once, high in the tree. Do not put
<ChatWidget />inside a route component that unmounts on navigation, or you will reset its state on every page change. - Pass user data only when you actually have it. Passing
undefineduntil your auth state resolves, then the real object, lets the widget associate conversations with a known person without you writing imperative update calls.
Hydration, SSR, and Next.js gotchas
If you are on Next.js (or any SSR/SSG framework), a chat widget is a client-only concern. It depends on window and document, which do not exist during server rendering. Rendering it on the server will throw, and trying to render its DOM during hydration will cause mismatches.
App Router (Next.js 13+)
Use the next/script component with the right strategy, in a Client Component:
```jsx
"use client";
import Script from "next/script";
export function ChatWidget() {
return (
<Script
src="https://cdn.aleeup.com/widget.js"
strategy="lazyOnload"
data-widget-id="your-widget-id"
/>
);
}
```
Key points:
- `strategy="lazyOnload"` tells Next.js to load the script during browser idle time, after the page is interactive. A chat bubble does not need to load before your hero section paints, and deferring it protects your Core Web Vitals.
- `"use client"` marks the boundary. The widget never runs on the server, so there is no hydration mismatch.
- Place
<ChatWidget />in your rootlayout.jsso it is present on every page without re-mounting per route.
If you must render markup conditionally
For components that render their own DOM (Option B packages) and would otherwise mismatch, guard with a mounted flag so the server and the first client render agree on "nothing," then mount on the client:
```jsx
"use client";
import { useState, useEffect } from "react";
import { ChatWidget } from "@vendor/react-chat";
export function ClientChat(props) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return null;
return <ChatWidget {...props} />;
}
```
This is the canonical fix for "Text content does not match server-rendered HTML" warnings caused by client-only widgets.
Lazy loading so the widget never hurts performance
A chatbot is, by definition, not above the fold and not needed in the first second. Loading its script eagerly is one of the most common, avoidable causes of a slow Largest Contentful Paint and a poor Interaction to Next Paint. Three escalating tactics:
- Defer to idle.
strategy="lazyOnload"in Next.js, orrequestIdleCallbackto append the script yourself, pushes loading until the main thread is free. - Load on first interaction. Render a lightweight placeholder bubble yourself (a static button, a few lines of CSS), and only inject the real widget script when the user clicks it or scrolls past a threshold. The vast majority of visitors never open chat, so most page loads pay zero widget cost.
- Load after a short delay or on intent. A
setTimeoutof a couple of seconds, or loading when the user's mouse approaches the corner where the bubble lives, balances availability against performance.
Here is the "load on first interaction" pattern, which gives you the best Lighthouse score:
```jsx
import { useState, useCallback } from "react";
function LazyChatLauncher() {
const [loaded, setLoaded] = useState(false);
const load = useCallback(() => {
if (loaded) return;
setLoaded(true);
const s = document.createElement("script");
s.src = "https://cdn.aleeup.com/widget.js";
s.async = true;
s.dataset.widgetId = "your-widget-id";
s.dataset.autoOpen = "true"; // open immediately once ready
document.body.appendChild(s);
}, [loaded]);
if (loaded) return null; // real widget takes over
return (
<button
className="chat-launcher"
aria-label="Open chat"
onClick={load}
>
Chat with us
</button>
);
}
```
You render a cheap button, the real bot loads only when someone actually wants to talk, and your initial bundle and main thread stay clean. For a broader look at placement and behavior decisions beyond React specifically, the guide on how to embed an AI chatbot on any website covers the non-framework concerns.
Wiring chatbot events into your React app
A widget that only answers questions is leaving value on the table. The point of putting an AI chatbot for React on a product or marketing site is usually twofold: deflect repetitive questions and capture leads. To do that well, you need the widget's events flowing into your own code.
Most platforms emit events you can subscribe to—when a conversation starts, when a lead submits an email, when the bot escalates to a human. Subscribe in an effect:
```jsx
useEffect(() => {
function onLead(e) {
const { email, name, transcript } = e.detail;
// send to your backend / CRM
fetch("/api/leads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, name, transcript }),
});
// fire your own analytics
window.gtag?.("event", "generate_lead", { method: "chatbot" });
}
window.addEventListener("alee:lead", onLead);
return () => window.removeEventListener("alee:lead", onLead);
}, []);
```
A few things to get right:
- Always remove the listener in cleanup. Otherwise, in development under Strict Mode you will register duplicate listeners and double-fire your analytics.
- Forward the transcript, not just the email. A lead is far more useful to your sales team with the conversation context attached—what the visitor asked, what the bot answered.
- Pass identity in, get qualified leads out. If you already know who the user is (logged in), feed that to the widget so conversations are tied to real accounts.
If lead capture is the main reason you are doing this, it is worth reading how lead-generation chatbots work to design the qualifying questions before you wire up the events—the integration is the easy half; the conversation design is what converts.
Training the bot on your content
The integration code above is the delivery mechanism. What makes a modern chatbot genuinely useful is what it knows, and that is a separate, server-side concern from your React code. A retrieval-augmented bot is trained on your material—help docs, product pages, PDFs, your knowledge base—so it answers in your voice with your facts instead of generic guesses.
With Alee specifically, the flow is: point it at your site or upload documents, it crawls and indexes them, and the same widget snippet you embedded now answers from your content. You change what the bot knows by updating its sources in the dashboard, not by shipping a React deploy. That separation is the whole appeal—your front-end team owns the widget mount, and whoever owns content owns the answers. If you are comparing approaches to building that knowledge layer, see building an AI chatbot trained on your website.
A note on regulated industries
If your React app is for a bank, insurer, clinic, law firm, or any financial or medical context, scope the bot deliberately. A content-trained chatbot is excellent at logistics and FAQs—hours, locations, "how do I reset my portal password," "what documents do I need to bring," "where is my appointment." It should not be positioned as giving medical, legal, or financial advice, and you should make that explicit in the bot's greeting and system instructions.
Concretely, in regulated contexts:
- Constrain the bot to procedural and informational questions; configure it to decline diagnosis, legal opinions, or personalized financial recommendations.
- Make human handoff a first-class path—the moment a conversation moves toward advice or an account-specific decision, the bot should offer to connect a person or capture a callback request.
- Keep the disclaimer visible, not buried, so visitors understand the bot's role.
Wired this way, the bot reduces support load on routine questions while keeping the high-stakes interactions with qualified humans, which is exactly where they belong.
Common pitfalls and how to avoid them
A quick reference for the mistakes that eat an afternoon:
- Two chat bubbles in development. Strict Mode double-invokes effects. Add the
getElementByIdidempotency guard shown above. This disappears in production but the guard is the correct fix, not disabling Strict Mode. - Widget disappears on navigation. You rendered it inside a route component that unmounts. Move it to the root, above the router.
- Hydration mismatch warnings in Next.js. The widget ran during SSR. Mark it
"use client", usenext/scriptwithlazyOnload, or gate render behind a mounted flag. - Tanked Lighthouse score after adding chat. You loaded the script eagerly. Defer to idle or load on first interaction.
- Analytics events firing twice. You added an event listener without cleanup, so Strict Mode registered it twice. Return a cleanup function from your effect.
- Bot does not know the current page. Your SPA does not reload. Call the vendor's page-change method on route change, as shown in
ChatRouteSync.
Picking a platform
Most mature chatbot platforms—Alee, Intercom, Crisp, and others—will work with the patterns in this article, because they all ultimately load a script or ship a component. The decision usually comes down to three questions: How easily can you train it on your content versus building scripted flows? How clean is the React/Next.js integration and the events API? And does the pricing match whether you want live human chat, an AI knowledge bot, or both?
Intercom and Crisp lean toward live-agent help desks with bots bolted on; tools built around retrieval-augmented generation lean toward answering from your documents automatically. If your goal is a bot that reads your site and answers without you scripting every branch, that RAG-first category—Alee included—is the one to evaluate. For a wider comparison of that category, the rundown of the best SiteGPT alternatives lays out the trade-offs.
Frequently asked questions
Should I use the embed script or a React component?
Use the embed script (Option A) for most cases—it mounts outside your React tree, survives route changes automatically, and is the most robust. Reach for an official React component (Option B) only when you need to pass dynamic, reactive data into the bot or conditionally show and hide it. Do not hand-roll a component wrapper around an embed script just for tidiness; the script loader is more reliable.
Why do I see two chat widgets in development but not production?
React 18 and 19 Strict Mode intentionally mounts, unmounts, and remounts components once in development to surface side-effect bugs, which can run your script-loading effect twice. Add an idempotency guard that checks whether the script element already exists before appending it. The duplication never happens in production, but the guard is the correct fix regardless.
How do I add a chatbot to a Next.js app without hydration errors?
Treat the widget as client-only. Use the next/script component with strategy="lazyOnload" inside a Client Component marked "use client", and place it in your root layout. For component-based widgets that render their own markup, gate them behind a mounted state flag set in useEffect so the server and first client render agree on rendering nothing.
Will adding an AI chatbot slow down my React app?
Only if you load it eagerly. Defer the script to browser idle time, or render a lightweight placeholder button and inject the real widget only when a user clicks it or scrolls. Since most visitors never open chat, this keeps the cost off nearly every page load and protects your Core Web Vitals.
How do I capture leads from the chatbot in my own system?
Subscribe to the widget's lead or conversation events in a useEffect, then forward the email, name, and full transcript to your backend or CRM, and fire your own analytics event. Always remove the listener in the effect's cleanup to avoid duplicate firing under Strict Mode. Sending the transcript along with the email gives your team the context that makes the lead actionable.
Can the chatbot answer questions specific to my product?
Yes—that is the point of a content-trained bot. Platforms like Alee index your help docs, product pages, and uploaded files, then answer from that material rather than generic knowledge. You update what the bot knows by changing its sources in the dashboard, with no React redeploy required, since the knowledge lives server-side and your widget snippet stays the same.
Ready to add a bot that actually knows your product? Start free with Alee, point it at your site or docs, and paste the widget into your React app in a few minutes—your front-end team owns the mount, the dashboard owns the answers, and your visitors get accurate, on-brand help from the first interaction.
Build your own AI chatbot with Alee
Train it on your site, embed it anywhere, capture leads 24/7. Free to start.