Documentation menu

Vite + React

A single-page app with React Router (or any router) and no server.

npm install @saaspro/react

1. Environment

# .env
VITE_SPM_KEY=spm_pub_prod_xxxxxxxx

Vite only exposes VITE_-prefixed variables to the bundle. A public key is designed to live there — never put a secret key in a browser build.

2. Provider

// src/main.tsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import { SaaSProMaxProvider } from "@saaspro/react";

import App from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <SaaSProMaxProvider
      config={{
        key: import.meta.env.VITE_SPM_KEY,
        release: __APP_VERSION__,
        env: import.meta.env.MODE,
      }}
    >
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </SaaSProMaxProvider>
  </StrictMode>,
);

The provider initialises once even under StrictMode's double-effects.

Expose the version from vite.config.ts if you want it on every event:

import { defineConfig } from "vite";
import pkg from "./package.json";

export default defineConfig({
  define: { __APP_VERSION__: JSON.stringify(pkg.version) },
});

3. Pageviews

The browser SDK already patches history.pushState / replaceState, so most routers produce pageviews with no extra code. To drive them explicitly from React Router:

// src/App.tsx
import { useLocation } from "react-router";
import { SPMPageview, SPMErrorBoundary } from "@saaspro/react";

export default function App() {
  const location = useLocation();
  return (
    <SPMErrorBoundary fallback={(error, reset) => <Crashed error={error} onRetry={reset} />}>
      <SPMPageview pathname={location.pathname} search={location.search} />
      <Routes>…</Routes>
    </SPMErrorBoundary>
  );
}

Set autoPageviews: false in the provider config if you drive them yourself and want to be sure nothing fires twice.

4. Events

import { TrackedButton, TrackOnView, useTrack, useIdentify } from "@saaspro/react";

function Pricing() {
  return (
    <TrackOnView event="pricing seen" as="section">
      <TrackedButton event="cta clicked" properties={{ plan: "pro" }}>
        Start free
      </TrackedButton>
    </TrackOnView>
  );
}

function useAuth() {
  const identify = useIdentify();
  const track = useTrack();

  async function signIn(email: string, password: string) {
    const user = await api.signIn(email, password);
    identify(user.id, { email: user.email, plan: user.plan });
    track("signed in", { method: "password" });
    return user;
  }
  …
}

On sign-out:

import { useSaaSProMax } from "@saaspro/react";
const spm = useSaaSProMax();
spm.reset();

5. Feature flags

import { FeatureFlag, useLoadFlags } from "@saaspro/react";

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

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

Values are cached in localStorage, so a returning visitor renders the last known variant immediately with no flash.

6. Errors

window errors and unhandled rejections are captured automatically. Add SPMErrorBoundary for render errors, and report caught ones by hand:

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

const report = useCaptureException();
try {
  await risky();
} catch (error) {
  report(error, { area: "upload" });
}

To make stack traces readable, build with source maps:

export default defineConfig({ build: { sourcemap: true } });
<SaaSProMaxProvider config={{ key: import.meta.env.VITE_SPM_KEY, consent: "pending" }}>
import { useSaaSProMax } from "@saaspro/react";

function CookieBanner() {
  const spm = useSaaSProMax();
  return (
    <div role="dialog">
      <button onClick={() => spm.consent("granted")}>Accept</button>
      <button onClick={() => spm.consent("revoked")}>Reject</button>
    </div>
  );
}

Nothing is sent or stored while consent is pending; granting flushes the buffer.

Checklist

  • VITE_SPM_KEY is a public key (spm_pub_…)
  • Provider wraps the router, not the other way round
  • spm.reset() on sign-out
  • build.sourcemap: true for readable stack traces
  • CSP (if any) allows connect-src https://saaspro.dev