Documentation menu

@saaspro/react

An SSR-safe provider, hooks, tracked components and an error boundary around @saaspro/browser.

npm install @saaspro/react

Peer dependencies: react >= 18. next >= 14 is optional and only needed for the @saaspro/react/next entry.

Provider

import { SaaSProMaxProvider } from "@saaspro/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SaaSProMaxProvider config={{ key: "spm_pub_prod_xxxxxxxx" }}>
      {children}
    </SaaSProMaxProvider>
  );
}

config takes every browser SDK option. The provider initialises in an effect, exactly once per client, and stays quiet during server rendering. A component that tracks on mount is safe: the SDK buffers calls made before initialisation and replays them.

Pageviews

Next.js App Router:

import { SPMPageview } from "@saaspro/react/next";

<SaaSProMaxProvider config={{ key: process.env.NEXT_PUBLIC_SPM_KEY!, autoPageviews: false }}>
  <SPMPageview />
  {children}
</SaaSProMaxProvider>

It reads usePathname() and useSearchParams() and wraps itself in Suspense, so it never forces the rest of your tree into client rendering.

Any other router:

import { SPMPageview } from "@saaspro/react";

<SaaSProMaxProvider config={{ key: import.meta.env.VITE_SPM_KEY, autoPageviews: false }}>
  <SPMPageview pathname={location.pathname} search={location.search} />
  {children}
</SaaSProMaxProvider>

<SPMPageview /> owns pageviews, so autoPageviews: false keeps the SDK's history patch from announcing the same navigation as well. The browser SDK also drops a $pageview for a path + search it announced less than a second ago, but treat that dedupe as a safety net rather than the configuration.

or the hook directly:

import { usePageview } from "@saaspro/react";
usePageview(router.pathname, router.search);

A pageview fires only when the path or query actually changes, so re-renders are free.

Hooks

import {
  useSaaSProMax, useTrack, useIdentify, usePage,
  useCaptureException, useErrorReporter,
  useFlag, useFlags, useFeatureEnabled, useLoadFlags,
} from "@saaspro/react";

function Checkout() {
  const track = useTrack();
  const identify = useIdentify();
  const report = useCaptureException();
  const variant = useFlag("checkout-copy", "control");

  async function submit(form: FormData) {
    track("checkout submitted", { variant: String(variant) });
    try {
      const user = await pay(form);
      identify(user.id, { email: user.email, plan: user.plan });
    } catch (error) {
      report(error, { tags: { area: "checkout" } });
    }
  }
  …
}

useErrorReporter() is the shorthand for the common case — report(error, { area: "checkout" }) takes the tags directly.

Every returned callback is stable, so it is safe in dependency arrays.

Loading flags once, high in the tree:

function FlagLoader({ user }: { user: User }) {
  useLoadFlags({ plan: user.plan, country: user.country });
  return null;
}

Everything below re-renders automatically when the values arrive.

Components

TrackedButton

<TrackedButton
  event="cta clicked"
  properties={{ position: "hero" }}
  value={49}
  currency="EUR"
  className="btn-primary"
  onClick={() => router.push("/signup")}
>
  Start free
</TrackedButton>

Sends the event, then calls your onClick. All other <button> props pass through.

TrackOnView

<TrackOnView event="pricing seen" properties={{ section: "plans" }} threshold={0.5} as="section">
  <PricingTable />
</TrackOnView>

Fires once when the block scrolls into view. repeat fires on every entry; as picks the wrapper element. Where IntersectionObserver is unavailable it fires on mount rather than losing the event.

SPMErrorBoundary

<SPMErrorBoundary
  tags={{ area: "checkout" }}
  fallback={(error, reset) => (
    <div role="alert">
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )}
>
  <Checkout />
</SPMErrorBoundary>

Reports the error as unhandled with mechanism: "boundary" plus the component stack, then renders the fallback. fallback can also be a plain node.

React requires error boundaries to be class components, so this one cannot read context — if you passed a custom client to the provider, pass the same one here.

FeatureFlag

<FeatureFlag flag="new-onboarding" fallback={<OldFlow />}>
  <NewFlow />
</FeatureFlag>

<FeatureFlag flag="checkout-copy" variant="urgent">
  <UrgentCopy />
</FeatureFlag>

Without variant the children render when the flag is true or a non-empty string. With variant they render only on an exact match.

Multiple clients

import { createClient, SaaSProMaxProvider } from "@saaspro/react";

const client = createClient();

<SaaSProMaxProvider client={client} config={{ key: "spm_pub_prod_xxxxxxxx" }}>
  …
</SaaSProMaxProvider>

Useful for tests, for a page that reports into two applications, and anywhere the shared singleton would collide.

Server-side events

Hooks only cover the browser. For events and errors raised on the server, add @saaspro/node — see the Next.js recipe.

Short-lived browser identity

Pass privacyMode: "ephemeral" in the provider config. It keeps tracking identity and flags in memory, ignores identify/alias, and resets within 30 minutes or on reload. Apply this at initial installation; existing provider instances initialize once. See Browser privacy modes for consent and reporting limits.