# Install on React (Vite or Create React App)

Add one script tag to your app's HTML page, publish, and open the site. This page covers a React single-page app built with Vite or Create React App, and apps that would rather load the tag from code. For Next.js, see [Install on Next.js](/docs/nextjs).

Platform steps checked against the official documentation on 2026-11-03: [Vite: index.html and project root](https://vite.dev/guide/#index-html-and-project-root), [React: Sunsetting Create React App](https://react.dev/blog/2025/02/14/sunsetting-create-react-app), [React: StrictMode](https://react.dev/reference/react/StrictMode).

## Quick install

Settings › Channels in the Telltale dashboard shows this tag with your real key already filled in. Copy it from there.

```html
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY"></script>
```

### Vite

1. Open `index.html` at the root of your project (Vite keeps it there, not in `public/`).
2. Paste the tag inside `<head>`, before the `<script type="module" src="/src/main.tsx">` line.
3. Build and deploy as you normally do.

```html
<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY"></script>
    <title>My app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

### Create React App

1. Open `public/index.html`.
2. Paste the tag inside `<head>`.
3. Run `npm run build` and deploy.

The React team deprecated Create React App for new apps on February 14, 2025; it still works in maintenance mode. If you move to Vite later, move the tag to Vite's `index.html`.

### Load it from code instead

If you cannot edit the HTML page (for example a micro-frontend), add the tag once from your root component. The check on the existing tag keeps React Strict Mode, which runs effects twice in development, from adding it twice.

```tsx
// src/useTelltale.ts
import { useEffect } from 'react';

const SDK_URL = 'https://app.trytelltale.dev/sdk/v1.js';

export function useTelltale() {
  useEffect(() => {
    if (document.querySelector(`script[src="${SDK_URL}"]`)) return; // already added
    const script = document.createElement('script');
    script.src = SDK_URL;
    script.dataset.telltaleKey = 'YOUR_SITE_PUBLISHABLE_KEY';
    document.head.append(script);
    // No cleanup: the tag stays for the life of the page. Removing it would not stop the SDK.
  }, []);
}
```

Call `useTelltale()` once in your top-level `App` component. The publishable key is meant for the browser; never put the server key (`ao_sk_...`) in your React code or in a `VITE_` or `REACT_APP_` variable.

## Consent

The tag records from page load. That is right for sites without a cookie or consent banner.

If your site shows a banner, hold the tag: add `data-telltale-wait-for-consent`. A held tag sends nothing at all until you call `optIn()`.

```html
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY" data-telltale-wait-for-consent></script>
```

In the code version, add `script.dataset.telltaleWaitForConsent = '';`.

Then pass the banner's decision on, wherever your banner reports it:

```ts
// When the banner allows measurement:
await window.Telltale.getClient().optIn();
// When the visitor refuses or withdraws:
await window.Telltale.getClient().optOut();
```

Apply the saved decision on every full page load, and again each time it changes. Signing in is not consent. For a consent manager, use the [Consent bridge guide](/docs/consent) and the [OneTrust](/docs/consent-onetrust), [Cookiebot](/docs/consent-cookiebot) or [Sourcepoint](/docs/consent-sourcepoint) recipe; [Install on Next.js](/docs/nextjs#react-single-page-application) shows the bridge in a React app.

## Identify the signed-in user

Telltale is most useful when each session is linked to the user and the account it belongs to. Send opaque, stable internal IDs, such as your database IDs. Never send e-mail addresses, names or phone numbers.

**From your server (recommended).** After sign-in, your backend links the session to the user it authenticated. This is the identity Telltale trusts. See [Quickstart step 2](/docs/quickstart#2-attach-identity-from-your-own-authentication) and [Server API › Associate identity](/docs/server#associate-identity).

**In the browser (a label).** Once your auth code knows who is signed in, call `identify()`. Call `reset()` on logout or when the user switches account.

```tsx
// src/TelltaleIdentity.tsx
import { useEffect, useRef } from 'react';
import { useAuth } from './auth'; // your own auth hook

declare global {
  interface Window {
    Telltale?: { getClient(): {
      identify(identity: { userId?: string | null; accountId?: string | null }): Promise<void>;
      reset(): Promise<void>;
      optIn(): Promise<void>;
      optOut(): Promise<void>;
    } };
  }
}

export function TelltaleIdentity() {
  const { user } = useAuth(); // null when signed out
  const linked = useRef<string | null>(null); // who this page last identified
  useEffect(() => {
    let client;
    try {
      client = window.Telltale?.getClient();
    } catch {
      return; // no Telltale client on this page
    }
    if (!client) return; // the tag was blocked or has not loaded
    const current = user ? `${user.id}:${user.accountId}` : null;
    if (current === linked.current) return;
    const wasSignedIn = linked.current !== null;
    linked.current = current;
    // Identify on sign-in and account switch; reset only on logout (not for visitors who never signed in).
    const done = user
      ? client.identify({ userId: user.id, accountId: user.accountId })
      : wasSignedIn ? client.reset() : null;
    // identify() rejects while the tag is held for consent, or when the Telltale server is unreachable.
    done?.catch(() => {});
  }, [user?.id, user?.accountId]);
  return null;
}
```

Render `<TelltaleIdentity />` once, inside your auth provider. With the tag in `index.html`, the SDK has loaded before your bundle runs. If you load it from code, `getClient()` can still be missing on the first render; the effect runs again when the user changes, and server identify does not depend on it. A held tag forgets identity until `optIn()`, so identify again after the visitor allows measurement.

## Content Security Policy

If your app sends a Content Security Policy, allow the Telltale server for scripts and for connections:

```http
Content-Security-Policy: script-src 'self' https://app.trytelltale.dev; connect-src 'self' https://app.trytelltale.dev
```

Or use a per-response nonce with `'strict-dynamic'`, and keep `connect-src https://app.trytelltale.dev`. Never add `'unsafe-inline'` or `'unsafe-eval'`. Vite dev servers usually send no policy, so test the policy on a production build. The [Content Security Policy guide](/docs/csp) has both options in full.

## Check it worked

1. In the Telltale dashboard, open Settings › Channels and leave the **Is it working?** check open.
2. Choose **Open my site to test it**, or open your deployed app in a new tab yourself. Browse a page or two.
3. Watch the check move through **Script loaded on** your address, **First session received** and, if you set up identify, **Signed-in user linked**. When the first session arrives, it links to it.

You can also look in your browser's DevTools › Network: `sdk/v1.js` should answer 200, and so should `api/sdk/init`. If it stays at the first step, see [It's not working](/docs/troubleshooting).

## Common mistakes

- **Testing on `localhost` against the live Site.** Your Site lists your live address. Add `http://localhost:5173` (Vite) or `http://localhost:3000` (Create React App) to a separate test Site, so test traffic never mixes with real numbers.
- **`www` and the bare domain.** `https://www.example.com` and `https://example.com` are different addresses. Add every one your app is served from under Settings › Channels › Website addresses, and staging domains too.
- **The tag in `src/` or in a component's JSX.** A `<script>` written in JSX does not run. Use `index.html`, or the hook above.
- **Two installs.** Do not combine this tag with Google Tag Manager or Cloudflare for the same Site on the same page.
- **Ad blockers.** Some privacy extensions block third-party scripts. Test in a window without extensions.
