React SDK

A provider and hooks around the same browser engine.

@pivolio/react wraps @pivolio/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 instead — it re-exports everything on this page and adds App Router page views.

Install

$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:

1import { PivolioProvider } from '@pivolio/react';
2
3export function App({ children }: { children: React.ReactNode }) {
4 return (
5 <PivolioProvider
6 config={{
7 writeKey: 'wk_live_xxxxxxxxxxxx',
8 apiUrl: 'https://api.pivolio.com',
9 }}
10 >
11 {children}
12 </PivolioProvider>
13 );
14}

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.

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.

1import { usePivolio } from '@pivolio/react';
2
3function ConsentBanner() {
4 const tracker = usePivolio();
5 return (
6 <button onClick={() => tracker?.setConsent({ ad_storage: 'granted' })}>
7 Accept
8 </button>
9 );
10}

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:

1import { useTrack } from '@pivolio/react';
2
3function CheckoutButton() {
4 const { track, trackPurchase, identify, page } = useTrack();
5
6 return (
7 <button
8 onClick={() =>
9 trackPurchase({ value: 99.5, currency: 'USD', orderId: 'ord_123' })
10 }
11 >
12 Pay
13 </button>
14 );
15}

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:

1import { usePageView } from '@pivolio/react';
2
3function ProductPage() {
4 usePageView();
5 return <main></main>;
6}

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:

1<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.

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