Vite + React
On this page
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 } });
7. Cookie banner#
<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_KEYis a public key (spm_pub_…) - Provider wraps the router, not the other way round
-
spm.reset()on sign-out -
build.sourcemap: truefor readable stack traces - CSP (if any) allows
connect-src https://saaspro.dev