> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivolio.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivolio.com/_mcp/server.

# React SDK

`@pivolio/react` wraps [`@pivolio/browser`](/documentation/sd-ks/browser) in a context provider so
a React tree owns exactly one `Tracker`, built at the right time in the lifecycle.

On Next.js, install [`@pivolio/next`](/documentation/sd-ks/next-js) instead — it re-exports
everything on this page and adds App Router page views.

## Install

```bash
npm install @pivolio/react
```

`@pivolio/browser` comes along as a dependency. You don't install it separately,
and you don't import from it — `@pivolio/react` re-exports the types, the enums
(`StandardEventName`, `ConsentStatus`, `DeviceType`), `PivolioError` and the
`Tracker` class.

## Setup

Wrap your app once, as high as you can:

```tsx
import { PivolioProvider } from '@pivolio/react';

export function App({ children }: { children: React.ReactNode }) {
  return (
    <PivolioProvider
      config={{
        writeKey: 'wk_live_xxxxxxxxxxxx',
        apiUrl: 'https://api.pivolio.com',
      }}
    >
      {children}
    </PivolioProvider>
  );
}
```

The provider is SSR-safe by construction: the `Tracker` is built inside an
effect, which never runs on the server. Server render and first client render
both see `null`, so the markup matches and hydration stays clean. Constructing it
during render would read cookies and mint an `anonymous_id` in an environment
that cannot persist one.

## Why the singleton API is not re-exported

`@pivolio/react` deliberately does **not** re-export `init`, `track`, `page`,
`identify` or any other singleton function from `@pivolio/browser`.

Those functions operate on a module-level instance created by `init()`. The
provider creates its own instance with `new Tracker(config)`. Import both into
one app and you get **two independent trackers** on the page: two sets of
listeners, two batch buffers, two page views per navigation, and — because each
starts its own session — attribution split across two session ids. Nothing
throws; the numbers are just quietly wrong.

> **Warning**
>
> Do not `import { track } from '@pivolio/browser'` inside a component tree that
> already renders a `PivolioProvider`. Use `useTrack()` or `usePivolio()`.

## Hooks

### usePivolio()

Returns the live `Tracker`, or `null` before the provider's effect has run.

```tsx
import { usePivolio } from '@pivolio/react';

function ConsentBanner() {
  const tracker = usePivolio();
  return (
    <button onClick={() => tracker?.setConsent({ ad_storage: 'granted' })}>
      Accept
    </button>
  );
}
```

Reach for this when you need something outside the four common calls —
`setConsent`, `optOut`, `getLinkerParam`, `getConsent`, `flush`. Always
null-check; the first render has no tracker yet.

### useTrack()

Stable callbacks for the four everyday operations:

```tsx
import { useTrack } from '@pivolio/react';

function CheckoutButton() {
  const { track, trackPurchase, identify, page } = useTrack();

  return (
    <button
      onClick={() =>
        trackPurchase({ value: 99.5, currency: 'USD', orderId: 'ord_123' })
      }
    >
      Pay
    </button>
  );
}
```

The returned object is memoized on the tracker, so it's safe in a `useEffect`
dependency array. Calls made before the tracker exists are **dropped, not
queued** — which matters mainly for tracking inside a mount effect on the very
first render.

### usePageView()

Fires one page view once the tracker is ready:

```tsx
import { usePageView } from '@pivolio/react';

function ProductPage() {
  usePageView();
  return <main>…</main>;
}
```

Client-only, since effects don't run on the server. For a router-driven app,
prefer wiring page views to your router's location rather than calling this in
every page component.

## Config identity and re-renders

The provider rebuilds its `Tracker` **only when `writeKey` or `apiUrl` change**.
Every other option is read through a ref.

That's what makes the obvious call site safe:

```tsx
<PivolioProvider config={{ writeKey, apiUrl, autoPageView: true }}>
```

An inline object literal is a new object on every parent render. If the effect
keyed on the object's identity, each parent render would tear down the tracker
and build a new one — rotating the session, re-running the `/id` fetch, and
re-firing a page view. Keying on the two values that genuinely identify a
workspace and collector avoids all of that, with no `useMemo` required of you.

> **Note**
>
> The consequence is that the remaining options are **init-time only**. Changing
> `debug` or `consent` in the config prop after mount does nothing. Change those at
> runtime through the tracker: `tracker.setConsent(...)`, `tracker.debug(true)`.

## Next steps

* [Browser SDK](/documentation/sd-ks/browser) — the full method surface on `Tracker`.
* [Configuration](/documentation/sd-ks/configuration) — every `SDKConfig` field.
* [Next.js](/documentation/sd-ks/next-js) — App Router page views.