Skip to main content
React

Add an AI Chatbot to Your React App

Load the SDK from the CDN, mount it in an effect, and unmount it in the cleanup. A drop-in component, with the StrictMode double-mount handled.

Priced in your local currency
No npm package, nothing in your bundle

The component

Copy this into your project, replace the key, and render it once in your app shell.

tsx
import { useEffect, useRef } from "react";

const CDN = "https://cdn.cortexvia.in";
const CHATBOT_KEY = "ctx-cb-xxxxxxxxxxxxxxxx";

/* One shared promise, so the CDN files are requested once per page even if the
 * component mounts twice (React StrictMode does exactly that in development). */
let sdkReady: Promise<unknown> | null = null;

function loadSdk() {
  if (sdkReady) return sdkReady;

  sdkReady = new Promise((resolve, reject) => {
    if ((window as any).ChatbotSDK) {
      resolve((window as any).ChatbotSDK);
      return;
    }

    const css = document.createElement("link");
    css.rel = "stylesheet";
    css.href = CDN + "/chatbotsdk-web.css";
    document.head.appendChild(css);

    const script = document.createElement("script");
    script.src = CDN + "/chatbotsdk-web.iife.js";
    script.async = true;
    script.onload = () => resolve((window as any).ChatbotSDK);
    script.onerror = () => reject(new Error("Cortexvia SDK failed to load"));
    document.head.appendChild(script);
  });

  return sdkReady;
}

export default function CortexviaChat() {
  const mountNode = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let cancelled = false;
    let handle: { unmount: () => void } | null = null;

    loadSdk().then((sdk: any) => {
      // The effect can be torn down before the CDN responds.
      if (cancelled || !mountNode.current || !sdk?.mount) return;

      handle = sdk.mount(mountNode.current, {
        chatbotKey: CHATBOT_KEY,
        config: { mode: "chatbot", position: "bottom-right" },
      });
    });

    return () => {
      cancelled = true;
      handle?.unmount();
    };
  }, []);

  return <div ref={mountNode} />;
}

The shared sdkReady promise keeps the CDN files to one request per page even when the component mounts twice, and the cleanup is what makes StrictMode safe.

  • Render it in a layout that survives navigation, not inside a route
  • The cancelled flag covers unmounting while the CDN request is in flight
  • Drop the TypeScript annotations if your project is plain JavaScript

Get these right

Everything that separates a clean integration from two launchers and a leak.

01

Load the SDK once

The CDN files should be requested once per page, not once per component instance. A module-scope promise gives every mount the same in-flight request.

02

Mount in an effect, unmount in the cleanup

mount returns a handle with unmount and update. Calling unmount in the effect's cleanup is what keeps a route change from leaving a widget behind.

03

StrictMode mounts effects twice

In development React 18 and 19 run mount, cleanup, mount. Without the cleanup that shows up as two launchers, and it will look like a Cortexvia bug.

04

Guard the async gap

The component can unmount while the CDN request is still in flight, so a cancelled flag prevents mounting into a node React has already discarded.

Keeping the handle

If you would rather control the widget from outside React, store the handle globally.

html
<script>
  window.cvChatHandle = window.ChatbotSDK.mount("#cv-chat", {
    chatbotKey: "ctx-cb-xxxxxxxxxxxxxxxx",
    config: {
      mode: "chatbot"
    }
  });
</script>

The handle exposes update and unmount. update takes the same shape as mount, so you can switch mode, placeholder, or styling without remounting.

  • update reconfigures in place; remounting is not required
  • unmount removes the widget and its DOM
  • Null the reference after unmount so a stale handle is never reused

Why Cortexvia

A CDN widget with local currency pricing, with the trust pages published up front.

01

No npm dependency

The widget ships from the CDN and attaches to window.ChatbotSDK, so there is no package to add to your bundle and no version to keep in step with React.

02

Out of your critical path

Because the loader is injected after mount, the widget never blocks hydration or competes with your app's first meaningful paint.

03

Styles stay isolated

The widget renders inside the container you hand it, so it does not inherit your CSS-in-JS or utility classes and your styles do not leak into it.

04

Local pricing, free tier to test

Plans are priced in your local currency, and the free tier is enough to wire it into a real app.

Questions

What developers ask before wiring the widget into an app.

More platforms

The same SDK works anywhere a script tag can go. Pick your platform for the exact steps.

01WordPressWPCode, footer.php, or a block-theme footer pattern.
02Shopifytheme.liquid, before the closing body tag.
03WixCustom Code, added to Body end. Needs a published site.
04SquarespaceCode Injection, Footer. Needs a Business plan or higher.
05WebflowSite Settings, Custom Code, Footer Code, then republish.
06ReactMount in an effect, unmount in the cleanup.
07Next.jsnext/script with afterInteractive, App or Pages Router.
08Plain HTMLPaste before the closing body tag. The baseline.

Get started

Start on the free tier with a document or two, copy the component, and render it in your app shell.

Mount in an effect, unmount in the cleanup. Get that pair right and the rest of the integration is a key and a config object.