SaaS Pro Max
Sign inStart free
Documentation menu

Vite + React

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

Shell
npm install @saaspro/react

1. Environment#

Shell
# .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#

TSX
// 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:

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

TSX
// 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#

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

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

5. Feature flags#

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

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

TypeScript
export default defineConfig({ build: { sourcemap: true } });
TSX
<SaaSProMaxProvider config={{ key: import.meta.env.VITE_SPM_KEY, consent: "pending" }}>
TSX
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