PRIV ProtocolPRIV Docs
Analytics SDK

React Integration

Use PRIV SDK with React and Next.js applications including hooks, context providers, and App Router patterns.

React Integration

Use PRIV SDK with React and Next.js applications.


Basic Setup with useEffect

The simplest way to use the SDK in React:

'use client'; // Required for Next.js App Router

import { useEffect } from 'react';
import { priv } from '@priv/sdk';

export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    // Initialize SDK
    priv.init({
      apiKey: process.env.NEXT_PUBLIC_PRIV_KEY!,
      endpoint: 'https://api.priv.io/v1',
      debug: process.env.NODE_ENV === 'development',
    });

    // Cleanup on unmount
    return () => {
      priv.shutdown();
    };
  }, []);

  return <>{children}</>;
}

Context Provider Pattern

For better React integration, create a context provider:

// lib/analytics-context.tsx
'use client';

import {
  createContext,
  useContext,
  useEffect,
  useState,
  useCallback,
  type ReactNode,
} from 'react';
import { priv } from '@priv/sdk';
import type { EventProperties, UserTraits } from '@priv/sdk';

interface AnalyticsContextType {
  track: (event: string, properties?: EventProperties) => void;
  page: (name?: string, properties?: EventProperties) => void;
  identify: (userId: string, traits?: UserTraits) => void;
  setConsent: (granted: boolean) => void;
  reset: () => void;
  isTracking: boolean;
}

const AnalyticsContext = createContext<AnalyticsContextType | null>(null);

interface AnalyticsProviderProps {
  children: ReactNode;
  apiKey: string;
  endpoint?: string;
}

export function AnalyticsProvider({
  children,
  apiKey,
  endpoint = 'https://api.priv.io/v1',
}: AnalyticsProviderProps) {
  const [isTracking, setIsTracking] = useState(false);

  useEffect(() => {
    priv.init({
      apiKey,
      endpoint,
      debug: process.env.NODE_ENV === 'development',
    });

    // Check if consent was previously given
    setIsTracking(priv.isTracking());

    return () => {
      priv.shutdown();
    };
  }, [apiKey, endpoint]);

  const track = useCallback((event: string, properties?: EventProperties) => {
    priv.track(event, properties);
  }, []);

  const page = useCallback((name?: string, properties?: EventProperties) => {
    priv.page(name, properties);
  }, []);

  const identify = useCallback((userId: string, traits?: UserTraits) => {
    priv.identify(userId, traits);
  }, []);

  const setConsent = useCallback((granted: boolean) => {
    priv.setConsent(granted);
    setIsTracking(granted);
  }, []);

  const reset = useCallback(() => {
    priv.reset();
  }, []);

  return (
    <AnalyticsContext.Provider
      value={{ track, page, identify, setConsent, reset, isTracking }}
    >
      {children}
    </AnalyticsContext.Provider>
  );
}

export function useAnalytics() {
  const context = useContext(AnalyticsContext);
  if (!context) {
    throw new Error('useAnalytics must be used within AnalyticsProvider');
  }
  return context;
}

Next.js App Router Integration

Layout Setup

// app/layout.tsx
import { AnalyticsProvider } from '@/lib/analytics-context';
import { ConsentBanner } from '@/components/consent-banner';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider apiKey={process.env.NEXT_PUBLIC_PRIV_KEY!}>
          {children}
          <ConsentBanner />
        </AnalyticsProvider>
      </body>
    </html>
  );
}

Page View Tracking

Track page views automatically on route changes:

// components/page-view-tracker.tsx
'use client';

import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { useAnalytics } from '@/lib/analytics-context';

export function PageViewTracker() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const { page, isTracking } = useAnalytics();

  useEffect(() => {
    if (!isTracking) return;

    // Track page view with URL info
    page(undefined, {
      path: pathname,
      search: searchParams.toString(),
    });
  }, [pathname, searchParams, page, isTracking]);

  return null;
}

Add to your layout:

// app/layout.tsx
import { Suspense } from 'react';
import { PageViewTracker } from '@/components/page-view-tracker';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <AnalyticsProvider apiKey={process.env.NEXT_PUBLIC_PRIV_KEY!}>
          <Suspense fallback={null}>
            <PageViewTracker />
          </Suspense>
          {children}
        </AnalyticsProvider>
      </body>
    </html>
  );
}

A simple consent banner using the context:

// components/consent-banner.tsx
'use client';

import { useState, useEffect } from 'react';
import { useAnalytics } from '@/lib/analytics-context';

export function ConsentBanner() {
  const { setConsent, isTracking } = useAnalytics();
  const [showBanner, setShowBanner] = useState(false);

  useEffect(() => {
    // Check if consent decision was already made
    const hasConsent = localStorage.getItem('analytics-consent');
    if (hasConsent === null) {
      setShowBanner(true);
    }
  }, []);

  const handleAccept = () => {
    localStorage.setItem('analytics-consent', 'true');
    setConsent(true);
    setShowBanner(false);
  };

  const handleReject = () => {
    localStorage.setItem('analytics-consent', 'false');
    setConsent(false);
    setShowBanner(false);
  };

  if (!showBanner) return null;

  return (
    <div className="fixed bottom-4 right-4 max-w-md bg-white rounded-lg shadow-lg p-6 border">
      <h3 className="font-semibold mb-2">We value your privacy</h3>
      <p className="text-sm text-gray-600 mb-4">
        We use cookies to improve your experience and analyze site usage.
      </p>
      <div className="flex gap-2">
        <button
          onClick={handleReject}
          className="px-4 py-2 text-sm border rounded hover:bg-gray-50"
        >
          Reject
        </button>
        <button
          onClick={handleAccept}
          className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
        >
          Accept
        </button>
      </div>
    </div>
  );
}

Event Tracking in Components

Track Button Clicks

'use client';

import { useAnalytics } from '@/lib/analytics-context';

export function SignupButton() {
  const { track } = useAnalytics();

  const handleClick = () => {
    track('signup_button_clicked', {
      location: 'hero',
      variant: 'primary',
    });
  };

  return (
    <button onClick={handleClick} className="btn-primary">
      Get Started
    </button>
  );
}

Track Form Submissions

'use client';

import { useAnalytics } from '@/lib/analytics-context';

export function ContactForm() {
  const { track } = useAnalytics();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    const startTime = performance.now();

    // Submit form...
    await submitForm();

    track('form_submitted', {
      form_id: 'contact',
      time_to_complete: Math.round(performance.now() - startTime),
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
    </form>
  );
}

Custom Track Hook

// hooks/use-track-click.ts
'use client';

import { useCallback } from 'react';
import { useAnalytics } from '@/lib/analytics-context';
import type { EventProperties } from '@priv/sdk';

export function useTrackClick(eventName: string, baseProperties?: EventProperties) {
  const { track } = useAnalytics();

  return useCallback(
    (additionalProperties?: EventProperties) => {
      track(eventName, { ...baseProperties, ...additionalProperties });
    },
    [track, eventName, baseProperties]
  );
}

// Usage
function CTAButton() {
  const trackClick = useTrackClick('cta_click', { location: 'header' });

  return (
    <button onClick={() => trackClick({ variant: 'primary' })}>
      Click Me
    </button>
  );
}

User Identification

On Login

'use client';

import { useAnalytics } from '@/lib/analytics-context';

export function LoginForm() {
  const { identify, track } = useAnalytics();

  const handleLogin = async (email: string, password: string) => {
    const user = await authService.login(email, password);

    // Identify the user
    identify(user.id, {
      email: user.email,
      name: user.name,
      plan: user.subscription?.plan,
      created_at: user.createdAt,
    });

    // Track the login
    track('user_logged_in', {
      method: 'email',
    });
  };

  return <form onSubmit={handleLogin}>{/* ... */}</form>;
}

On Logout

'use client';

import { useAnalytics } from '@/lib/analytics-context';

export function LogoutButton() {
  const { track, reset } = useAnalytics();

  const handleLogout = async () => {
    track('user_logged_out');

    await authService.logout();

    // Reset analytics identity
    reset();
  };

  return <button onClick={handleLogout}>Log Out</button>;
}

Server Components Note

The PRIV SDK is client-side only. For Next.js App Router, ensure you use the 'use client' directive on any component that uses the SDK.

// This will NOT work in a Server Component
import { priv } from '@priv/sdk'; // Error!

// Correct: Mark as client component
'use client';

import { priv } from '@priv/sdk'; // Works!

If you need to track server-side events (API routes, server actions), use the API directly:

// app/api/webhook/route.ts
export async function POST(request: Request) {
  // Use the REST API for server-side tracking
  await fetch('https://api.priv.io/v1/events', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': process.env.PRIV_SECRET_KEY!,
    },
    body: JSON.stringify({
      type: 'track',
      event: 'webhook_received',
      properties: { source: 'stripe' },
    }),
  });
}

TypeScript Support

Full type safety with the analytics context:

import type { EventProperties, UserTraits } from '@priv/sdk';

interface PurchaseProperties extends EventProperties {
  product_id: string;
  price: number;
  currency: 'USD' | 'EUR' | 'GBP';
}

function usePurchaseTracking() {
  const { track } = useAnalytics();

  return (properties: PurchaseProperties) => {
    track('purchase', properties);
  };
}

Performance Tips

  1. Initialize once - Put the provider at the root of your app
  2. Memoize callbacks - Use useCallback for event handlers
  3. Batch events - The SDK batches automatically, no need to debounce
  4. Use Suspense - Wrap PageViewTracker in Suspense to avoid hydration issues
// Optimal setup
<AnalyticsProvider apiKey={apiKey}>
  <Suspense fallback={null}>
    <PageViewTracker />
  </Suspense>
  {children}
</AnalyticsProvider>

Next Steps