Skip to content

React (Vite, React 19, custom)

For a plain React app without a framework, the simplest install is a static <script> tag in index.html. The shell loads once, the script runs once, Statable handles the rest. React's render lifecycle is not involved.

If you use a framework, see Next.js for SSR and the App Router. This page covers non-framework setups: Vite, React 19 SPA, or any custom build.

Create React App was deprecated by the React team in February 2025. New projects should use Vite. Existing CRA projects continue to work and the snippets below apply unchanged.

Install

Static index.html (Vite, custom builds)

In a Vite project, index.html lives in the project root. Open it and add the script tag inside <head>:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My App</title>
    <script src="https://statable.com/js/YOUR_SITE_ID/s.js" defer></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Replace YOUR_SITE_ID with the value from Site settings → Tracking Code in your dashboard.

s.js is the single tracker bundle (~2.5 KB gzipped) — pageviews, engagement, scroll depth, outbound clicks, file downloads, and the window.statable API.

For Create React App, the file lives at public/index.html. Same script tag, same placement.

Build-time site ID via Vite env vars

Vite replaces %VITE_*% tokens in index.html at build time. In .env:

VITE_STATABLE_ID=abc123

In index.html:

<script src="https://statable.com/js/%VITE_STATABLE_ID%/s.js" defer></script>

CRA uses %REACT_APP_STATABLE_ID% with the same syntax.

React 19 component (built-in <script>)

React 19 hoists <script> elements rendered inside components to the document <head> and de-duplicates by src when async is set. Use this when you cannot edit the static HTML, for example in micro-frontends or library-rendered shells.

// src/components/StatableScript.tsx
export function StatableScript() {
  return (
    <script
      src={`https://statable.com/js/${import.meta.env.VITE_STATABLE_ID}/s.js`}
      async
    />
  )
}

Mount it once near the root:

// src/App.tsx
import { StatableScript } from './components/StatableScript'

export default function App() {
  return (
    <>
      <StatableScript />
      {/* rest of the tree */}
    </>
  )
}

For older React (18 and below), use react-helmet-async v3 instead:

import { Helmet } from 'react-helmet-async'

export function StatableScript() {
  return (
    <Helmet>
      <script
        src={`https://statable.com/js/${import.meta.env.VITE_STATABLE_ID}/s.js`}
        defer
      />
    </Helmet>
  )
}

Tracking custom events

Add a TypeScript declaration for the global:

// src/types/statable.d.ts
declare global {
  interface Window {
    statable: {
      t: (event: string, props?: Record<string, unknown>) => void
    }
  }
}
export {}

Fire events from any component:

import { useEffect, useState } from 'react'

export function PricingModal() {
  const [open, setOpen] = useState(false)

  useEffect(() => {
    if (open) window.statable?.t?.('Modal Opened', { name: 'pricing' })
  }, [open])

  return <button onClick={() => setOpen(true)}>See pricing</button>
}

The optional chaining (?.t?.) silently no-ops during the window between page parse and script execution. If you depend on the event being recorded, fire it after DOMContentLoaded.

Tracking page views in SPA

If you use React Router or any client router built on the History API, Statable detects pushState and replaceState automatically and counts each route as a pageview. No router subscription, no manual call, no plugin.

Excluding your own visits

In the browser console, run:

localStorage.setItem('analytics_ignore', 'true')

The tracker checks this flag on every event and skips reporting for that browser. Remove it with localStorage.removeItem('analytics_ignore').

Verify it works

  • Open Statable Realtime in the dashboard.
  • Load your dev server, click around. Sessions appear within seconds.
  • See Verify installation for the full procedure.

Common pitfalls

  • Calling window.statable before the script loads. With defer, the script executes after the parser finishes. With async, timing is unpredictable. Use the queue stub or optional chaining.
  • Mounting the script tag inside a frequently re-rendering component. Place it once at the app root or in index.html. React 19 de-duplicates by src when async is set, but earlier versions do not.
  • Forgetting the env var prefix. Vite needs VITE_, CRA needs REACT_APP_. Without the prefix, the value is undefined on the client.
  • Adding the tag in <body> of a Vite project. Place it in <head> so it loads in parallel with your bundle.

See also: Install the tracking script, Custom events, JavaScript API, Next.js.


Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.