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