# Remix

Add ProductLense to a Remix or React Router framework app in app/root.tsx, then identify signed-in users from a loader.

In Remix (and React Router in framework mode), every page renders through `app/root.tsx`, so the script goes in its `<head>` once. Client-side navigations use the history API and are recorded as pageviews automatically.

## Add the script

Replace `SITE_ID` with the id from Settings → Sites in the ProductLense dashboard.

```tsx title="app/root.tsx" {9-14}
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
        <script
          dangerouslySetInnerHTML={{
            __html: 'window.productlense=window.productlense||function(){(window.productlense.q=window.productlense.q||[]).push(arguments)}',
          }}
        />
        <script defer data-site="SITE_ID" src="https://api.productlense.app/a.js" />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}
```

The inline stub is a fixed string with no user data in it. If your site sends a Content-Security-Policy, allow it by hash or nonce; see [Content Security Policy](/docs/install/script-tag#content-security-policy). In React Router v7 framework mode the imports come from `react-router` instead of `@remix-run/react`; the tags are the same.

## Identify users

Return the user id from the root loader and call `identify` once it is known:

```tsx title="app/root.tsx" {6-8}
import { useEffect } from 'react';
import { useLoaderData } from '@remix-run/react';

export function ProductLenseIdentify() {
  const { userId } = useLoaderData<{ userId: string | null }>();
  useEffect(() => {
    if (userId) window.productlense?.('identify', userId);
  }, [userId]);
  return null;
}
```

Render `<ProductLenseIdentify />` inside `<body>`. Use the same id you store on the customer in your billing provider; see [how payments are attributed](/docs/revenue/attribution).

## Custom events

```tsx
<button onClick={() => window.productlense?.('event', 'Upgrade clicked', { plan: 'growth' })}>Upgrade</button>
```

See [custom events](/docs/track/custom-events) for naming rules.

## Verify your installation

1. Build and deploy, then open your site in a normal browser window.
2. Open the Live page in the ProductLense dashboard. Your visit appears within a few seconds.
3. In the browser's developer tools, the Network tab shows a `POST` to `https://api.productlense.app/api/e` with status `204`. Follow a link to another route: a second pageview follows.

The development server runs on `localhost`, where nothing is sent unless you add `data-dev="true"` to the tag. Ad blockers in your own browser may block the request. If the visit does not appear, follow [Verify your installation](/docs/install/verify).
