Stickybeak Docs Dashboard Get early access

Frameworks

React Router and Remix

React Router analytics with Stickybeak starts by adding its script to your rendered pages; this guide shows you where it belongs and how to track page views, goals and purchase revenue.

For library mode with Vite, use the React guide.

Add the script

  1. Put your site ID in an environment variable

    Set VITE_STICKYBEAK_SITE_ID in .env.production or production host settings.

    VITE_STICKYBEAK_SITE_ID=YOUR_SITE_ID
  2. Add the tag to the Layout in app/root.tsx

    import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
    
    const siteId = import.meta.env.VITE_STICKYBEAK_SITE_ID;
    
    export function Layout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <Meta />
            <Links />
            {siteId && (
              <script
                defer
                data-site={siteId}
                data-domain="example.com"
                src="https://app.usestickybeak.com/_cfw/s.js"
              />
            )}
          </head>
          <body>
            {children}
            <ScrollRestoration />
            <Scripts />
          </body>
        </html>
      );
    }
    
    export default function App() {
      return <Outlet />;
    }

    In Remix v2 the file is the same; the imports come from @remix-run/react.

  3. Build and deploy

    In react-router dev the variable isn't set, so nothing loads.

This also works with ssr: false (SPA mode) and prerendering: the root layout is still rendered into the HTML file.

Check it's working

Test the deployed site, then verify your install.

Track goals

Add data-goal to rendered links or buttons. For other events, use the typed queued helper below. See Custom goals.

<Link to="/signup" data-goal="start_signup" data-goal-plan="pro">Start free trial</Link>
type GoalProps = Record<string, string | number | boolean>;
type Stickybeak = ((goal: string, props?: GoalProps) => void) & { q?: unknown[][] };

declare global {
  interface Window {
    stickybeak?: Stickybeak;
  }
}

export function track(goal: string, props?: GoalProps): void {
  if (typeof window === "undefined") return; // on the server: nothing to do
  if (!window.stickybeak) {
    // Hold calls made before the script loads; it sends them when it arrives.
    const queue: Stickybeak = (...args) => {
      (queue.q ??= []).push(args);
    };
    window.stickybeak = queue;
  }
  window.stickybeak(goal, props);
}
import { useEffect } from "react";
import { Form, useActionData } from "react-router";
import { track } from "~/lib/stickybeak";
import type { Route } from "./+types/newsletter";

export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  await subscribe(String(form.get("email")));
  return { subscribed: true };
}

export default function Newsletter() {
  const data = useActionData<typeof action>();

  useEffect(() => {
    if (data?.subscribed) track("newsletter_signup");
  }, [data]);

  return (
    <Form method="post">
      <input type="email" name="email" required />
      <button>Subscribe</button>
    </Form>
  );
}

Track purchases

After confirming an order in the browser, send:

window.stickybeak?.("purchase", { revenue: 49, currency: "USD", order_id: "ORDER_ID" });

Use real values; see Revenue.

Page changes

Links, navigate(), action redirects, back and forward count new paths. Replace, query-only and hash-only changes do not. See Single-page apps.

If it isn't working

Check the production site ID and domain, then see Troubleshooting.