Frameworks · 2 min setup

How to add analytics to a React app

To add VisitTrack to a React app, paste the script tag into the <head> of index.html — not into a component. It loads once, and React Router (or any router using the History API) has every route change counted as a pageview without extra code.

Updated

A client-rendered React app has one HTML document: index.html at the project root in Vite, or public/index.html in a Create React App project. That's the shell every route renders into, so it's the right place for the tag. Putting it in a useEffect would re-add it on remounts and is harder to keep to exactly one copy.

Using Next.js or React Router's framework mode (formerly Remix)? Use the Next.js guide or the Remix guide instead — those render the <head> from a root component.

React at a glance

Where the tag goes
index.html (Vite) or public/index.html (Create React App)
Route changes
Tracked automatically with BrowserRouter / createBrowserRouter
Hash routing
HashRouter changes aren't separate pageviews (see below)
Custom events
window.visitrack("name", props)
Size
~5 KB gzipped, loaded with defer

How to install VisitTrack on React

  1. Step 1: Copy your site id

    Add your site in VisitTrack and copy the snippet from the install step (also in Settings → General). Replace SITE_ID below with your id.

  2. Step 2: Paste the tag into index.html

    Add it inside <head>. defer keeps it from blocking the first render; the script runs after the HTML is parsed and before DOMContentLoaded.

    index.html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My app</title>
        <script defer data-site="SITE_ID" src="https://visitrack.app/tracker.js"></script>
      </head>
      <body>
        <div id="root"></div>
        <script type="module" src="/src/main.tsx"></script>
      </body>
    </html>
  3. Step 3: Optional: a different site id per environment

    Localhost is already ignored. To keep staging out of your numbers, the simplest option is Settings → General → Allowed hostnames: once you save your production hosts, hits from any other hostname are dropped. If you'd rather give staging its own VisitTrack site, Vite replaces %VITE_*% placeholders in index.html at build time — set VITE_VISITRACK_SITE_ID for every build, because an unset variable is left in the HTML as literal text.

    index.html (Vite env placeholder)
    <script defer data-site="%VITE_VISITRACK_SITE_ID%" src="https://visitrack.app/tracker.js"></script>
  4. Step 4: Build, deploy and browse

    Run your production build, deploy it, and click through a few routes on the live URL.

How to check VisitTrack is working on React

  1. Open the deployed app and navigate between two routes with your in-app links (no full reload).
  2. The live view in your VisitTrack dashboard shows you within a few seconds; the Pages tab lists both routes.
  3. DevTools → Network: each route change sends a POST to visitrack.app/api/collect.
  4. Nothing arriving? Add data-debug to the tag and read the [visitrack] lines in the console.

React Router's BrowserRouter and data routers (createBrowserRouter) change the URL with history.pushState. The tracker wraps pushState and replaceState and listens for popstate, so <Link>, useNavigate() and the back button all produce pageviews. Pageviews are keyed on location.pathname: query-string-only changes (filters, ?page=2) aren't counted again.

HashRouter keeps the route after # (/#/settings), and the pathname never changes — so only the first load is recorded. If you need per-route numbers, switch to BrowserRouter (your host must serve index.html for unknown paths) or send a custom event per screen.

How to track custom events and goals in React

Call window.visitrack(name, props) right after the thing actually happened — after your API answers ok, not on click. Props are optional: up to 10 keys, lowercase a-z0-9_-, values up to 255 characters.

For simple clicks, data-vt-goal="pricing_cta" on any element records an event with no handler. data-vt-scroll="pricing_viewed" fires once when that element is at least half visible.

global.d.ts + usage
// global.d.ts
declare global {
  interface Window {
    visitrack?: ((name: string, props?: Record<string, unknown>) => void) & {
      identify?: (userId: string, traits?: Record<string, unknown>) => void;
      visitorId?: () => string;
    };
  }
}
export {};

// src/hooks/useTrack.ts
export function track(name: string, props?: Record<string, unknown>) {
  window.visitrack?.(name, props);
}

// in a component
<button onClick={async () => {
  const ok = await saveProject();
  if (ok) track("project_created", { template: "blank" });
}}>Create project</button>

After login, call window.visitrack?.identify?.(user.id, { plan: user.plan }) to tie the visitor to your user id; they then appear in People. See People & identify.

Script options you might need on React

All optional — add them to the same tag. Full reference: script configuration.

AttributeWhat it does
data-cookielessStore nothing in the browser; the server derives a daily-rotating id. See cookieless mode.
data-exclude="/app/*"Never track these paths (comma-separated, * wildcard). Checked on every navigation.
data-allow-localTrack on localhost and *.local — for testing an install only.
data-debugLog every event the script sends, and why it skipped one, to the console.
data-auto="false"Turn off automatic events (downloads, contact clicks, form submits, rage clicks, JS errors, 404s).

React troubleshooting

Only the first page shows up

You're probably on HashRouter; hash changes don't change the pathname. Use BrowserRouter, or track screens with custom events.

Every pageview is counted twice

The tag is in index.html and also injected from a component or a second template. Keep the single copy in index.html.

Events from useEffect fire twice in development

That's React Strict Mode double-invoking effects in dev only. Production builds run effects once — and localhost isn't tracked anyway unless you add data-allow-local.

TypeError: window.visitrack is not a function

A component called it before the deferred script loaded, or a blocker stopped it. Use optional chaining (window.visitrack?.()), or add the queue stub from Script configuration.

React analytics FAQ

Is there a React package or hook for VisitTrack?

No, and you don't need one. The script tag in index.html handles pageviews and route changes; custom events are a single global function, window.visitrack(). A three-line track() helper like the one above is all most apps add.

Does VisitTrack work with React Router v6 and v7?

Yes. Both use the History API in browser routing mode, which the tracker listens to. Declarative <BrowserRouter>, createBrowserRouter and v7's framework mode all work; only hash routing needs custom events per screen.

Where do I put the script in Create React App?

In public/index.html, inside <head>. Create React App is no longer maintained, but the same tag works unchanged after you migrate to Vite (index.html at the project root).

Can I track signups in a React app?

Yes. Call window.visitrack("signup") after your signup request succeeds; an event named signup fills the Signups tab with source and time-to-conversion. See custom events.

Does VisitTrack work with a React app behind a login?

Yes, but you may not want to count it with your marketing site. Add data-exclude="/app/*" to skip those paths, or give the app its own VisitTrack site id.

Is VisitTrack a good Google Analytics alternative for React?

If you want simple, cookie-free numbers with revenue attribution, yes — and it installs with one tag instead of gtag plus a router listener. See VisitTrack vs Google Analytics for an honest comparison.

Keep going

Add VisitTrack to your React site

Cookie-free analytics with revenue attribution, live visitors, funnels and session replays. One script tag, every feature on every plan. 14 days free, no card required.

Not on React? See all 29 integration guides.