- Docs
- Install
Install in React & Next.js
Add the TalkingDot messenger to a React app built with Vite or Create React App, or to a Next.js site using either router. This page also covers identifying signed-in users and loading the script only once.
Before you start: copy your workspace key from Settings → Install. It's the 16-character data-key value. The key is public, because it's in every page that shows the messenger, so it can live in a public environment variable such as VITE_TALKINGDOT_KEY or NEXT_PUBLIC_TALKINGDOT_KEY. Your workspace secret (Settings → Security) is different: it must never be bundled into client code.
Quickest option: index.html
For a client-rendered React app, the simplest install is the plain snippet in your HTML shell. With Vite that's index.html in the project root. With Create React App it's public/index.html.
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="https://talkingdot.com/widget.js" data-key="YOUR_WORKSPACE_KEY" async></script>
</body>That's all you need. The script loads once per page load and lives outside React, so re-renders and StrictMode don't affect it. Route changes are tracked automatically, as described in Route changes.
Load it from code (TypeScript)
If you'd rather keep the key in an environment variable, or call the messenger from components, add this small helper. It defines the window types, loads widget.js only once, and queues calls made before the script has finished loading.
const WIDGET_SRC = 'https://talkingdot.com/widget.js';
type TalkingDotFn = ((...args: any[]) => any) & { q?: unknown[][] };
declare global {
interface Window {
TalkingDot?: TalkingDotFn;
TalkingDotSettings?: Record<string, unknown>;
}
}
/** The messenger API, or a queue that widget.js replays once it has loaded. */
function api(): TalkingDotFn {
if (!window.TalkingDot) {
const q: unknown[][] = [];
window.TalkingDot = Object.assign((...args: unknown[]) => { q.push(args); }, { q });
}
return window.TalkingDot;
}
/** talkingdot('open'), talkingdot('update', {...}), talkingdot('shutdown') … */
export function talkingdot(...args: unknown[]): unknown {
if (typeof window === 'undefined') return undefined; // server render: nothing to do
return api()(...args);
}
/** Adds widget.js to the page once. Safe under StrictMode, hot reload and re-mounts. */
export function loadTalkingDot(key: string, settings?: Record<string, unknown>): void {
if (typeof window === 'undefined' || document.getElementById('talkingdot-js')) return;
if (settings) window.TalkingDotSettings = settings;
api(); // create the queue so early calls aren't lost
const s = document.createElement('script');
s.id = 'talkingdot-js';
s.async = true;
s.src = WIDGET_SRC;
s.setAttribute('data-key', key);
document.head.appendChild(s);
}Call it once from your root component. Put it in the app shell, not in individual routes:
import { useEffect } from 'react';
import { loadTalkingDot } from './talkingdot';
export default function App() {
useEffect(() => {
loadTalkingDot(import.meta.env.VITE_TALKINGDOT_KEY); // CRA: process.env.REACT_APP_TALKINGDOT_KEY
}, []);
return <main>{/* your routes */}</main>;
}Next.js: App Router
Use next/script in the root layout with strategy="afterInteractive". The layout persists across navigations, so the script loads once.
import Script from 'next/script';
import type { ReactNode } from 'react';
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://talkingdot.com/widget.js"
data-key={process.env.NEXT_PUBLIC_TALKINGDOT_KEY}
strategy="afterInteractive"
/>
</body>
</html>
);
}Next.js: Pages Router
Add the same <Script> to pages/_app.tsx. Alternatively, put a plain <script async> tag inside <body> in pages/_document.tsx.
import type { AppProps } from 'next/app';
import Script from 'next/script';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script
src="https://talkingdot.com/widget.js"
data-key={process.env.NEXT_PUBLIC_TALKINGDOT_KEY}
strategy="afterInteractive"
/>
</>
);
}Identify signed-in users
Once your auth state is known, call TalkingDot('update', …) with the user's details and a user_hash computed on your server. When someone signs out, call TalkingDot('shutdown'). That ends their messenger session in this browser and starts a fresh anonymous one, so the next person can't see their conversations. This component handles both cases and works with any auth library:
import { useEffect } from 'react';
import { talkingdot } from './talkingdot';
export type ChatUser = { id: string; email: string; name: string; talkingdotHash: string };
const FLAG = 'talkingdot_identified';
/** user: undefined while your auth check is loading, null when signed out. */
export function TalkingDotIdentity({ user }: { user: ChatUser | null | undefined }) {
const loading = user === undefined;
const id = user?.id, email = user?.email, name = user?.name, hash = user?.talkingdotHash;
useEffect(() => {
if (loading) return;
if (id) {
talkingdot('update', { user_id: id, email, name, user_hash: hash });
localStorage.setItem(FLAG, '1');
} else if (localStorage.getItem(FLAG)) {
localStorage.removeItem(FLAG); // signed out (here, in another tab, or the session expired)
talkingdot('shutdown');
}
}, [loading, id, email, name, hash]);
return null;
}Render <TalkingDotIdentity user={currentUser} /> once near the root. Compute the hash on the server, where your workspace secret lives. Here's an example for the Next.js App Router. Add 'use client' as the first line of TalkingDotIdentity.tsx when you use it there.
import { createHmac } from 'node:crypto';
import type { ReactNode } from 'react';
import { getCurrentUser } from '@/lib/auth'; // your own auth helper
import { TalkingDotIdentity } from '@/components/TalkingDotIdentity';
export default async function RootLayout({ children }: { children: ReactNode }) {
const me = await getCurrentUser();
const user = me
? {
id: String(me.id),
email: me.email,
name: me.name,
// Server only: never expose TALKINGDOT_SECRET as NEXT_PUBLIC_*.
talkingdotHash: createHmac('sha256', process.env.TALKINGDOT_SECRET!)
.update(String(me.id))
.digest('hex'),
}
: null;
return (
<html lang="en">
<body>
{children}
<TalkingDotIdentity user={user} />
{/* …the <Script> tag from above… */}
</body>
</html>
);
}In a Vite or CRA app, have your API return talkingdotHash with the current user, for example from /api/me, and pass that object in. See Identity verification for hash examples in other languages.
Route changes
You don't need to do anything on navigation. The messenger watches the History API (pushState, replaceState and popstate) and hash changes. React Router, TanStack Router and Next.js navigations are all recorded as page views. Page rules and proactive messages are checked again on each route. Don't load the script per route, and don't call shutdown on navigation.
StrictMode and double loading
In development, React StrictMode runs effects twice. The helper above checks for an existing #talkingdot-js element first. As a second safeguard, widget.js sets window.__talkingdotLoaded and ignores any later copy. Don't remove the script in an effect cleanup. The messenger is meant to live for the whole page session, and removing the tag doesn't unload it. To hide it on certain screens, use TalkingDot('hideLauncher') and TalkingDot('showLauncher'), or the page rules in Settings → Messenger.
Server-side rendering
Only touch window, document or localStorage inside useEffect, event handlers or client components. Code that runs during a server render has no browser. The helper returns early on the server, and next/script only runs in the browser, so neither breaks SSR or static export.
Verify it works
- Run your app and open it in a private window. The launcher bubble appears in the bottom-right corner, or on whichever side you chose.
- Navigate between a few routes, then send a test message. In the Inbox, the contact sidebar lists the pages you visited.
- Sign in, then sign out. The conversation shows your user's details while you're signed in. After signing out, the messenger starts a fresh, empty session.
Troubleshooting
TalkingDot: missing or invalid data-keyin the console: the environment variable is empty. Vite only exposes variables that start withVITE_, CRA withREACT_APP_, and Next.js withNEXT_PUBLIC_. Restart the dev server after editing.env.- Nothing on localhost: if you set a domain allow-list in Settings → Security, add
localhost(ports don't matter) and your preview domains, such as Vercel or Netlify preview URLs. window is not defined: code that toucheswindowran during SSR. Move it into an effect or a client component.- Content-Security-Policy: if you set CSP headers, allow
https://talkingdot.cominscript-src,connect-srcandimg-src, and'unsafe-inline'instyle-src.
Next steps
- JavaScript API: open the messenger from your own button, listen for unread counts and track events.
- Identity verification: compute
user_hashand require it. - Webhooks and REST API: sync conversations and contacts with your backend.