SignaKitdocs
Framework Guides

React SPA

Integrate SignaKit feature flags into a Vite or Create React App single-page application — set up SignaKitProvider, resolve a stable visitor ID, evaluate flags with useFlag, and track conversions.

React SPA

SDK: @signakit/flags-react + @signakit/flags-browser Environments: Vite, Create React App, or any bundler — purely client-side

In a React SPA there is no server. All flag evaluation happens in the browser: the SDK fetches your project config, evaluates rules locally, and persists a stable visitor ID in localStorage. This guide covers setup from scratch through conversion tracking.


Setup

Install the SDKs

npm install @signakit/flags-react @signakit/flags-browser

Create a visitor ID utility

SignaKit uses userId as the bucketing key — the same ID always produces the same variation. For anonymous visitors in a SPA, generate a UUID on first visit and persist it in localStorage so the same user always gets the same variation across sessions.

src/lib/visitor.ts
const VISITOR_KEY = 'sk_visitor_id'

export function getVisitorId(): string {
  const existing = localStorage.getItem(VISITOR_KEY)
  if (existing) return existing

  // crypto.randomUUID() is available in all modern browsers
  const id = crypto.randomUUID()
  localStorage.setItem(VISITOR_KEY, id)
  return id
}

If your app has authenticated users, skip this and use your auth session's user ID instead — authenticated IDs produce more reliable experiment results because they survive browser clears and cross-device use.

Wrap your app root with SignaKitProvider

Call getVisitorId() before rendering and pass the result to the provider. Because getVisitorId() is synchronous, you can call it directly in main.tsx.

src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { SignaKitProvider } from '@signakit/flags-react'
import { getVisitorId } from './lib/visitor'
import App from './App'

const visitorId = getVisitorId()

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <SignaKitProvider
      sdkKey="sk_browser_yourOrgId_yourProjectId_random"
      userId={visitorId}
      loadingFallback={<div>Loading…</div>}
    >
      <App />
    </SignaKitProvider>
  </StrictMode>
)

Use a browser SDK key, not sk_prod_

SDK keys beginning with sk_prod_ are server-side keys — they carry elevated permissions and must never appear in a browser bundle. Use a browser-appropriate key from your SignaKit project settings. The browser key is intentionally safe to expose; it can only read your flag config, not write to your project.

Pass user attributes for targeting

If you have attributes at render time (from a stored profile, query string, or feature-detection), pass them to the provider so targeting rules can match against them.

src/main.tsx
import { StrictMode, useMemo } from 'react'
import { createRoot } from 'react-dom/client'
import { SignaKitProvider } from '@signakit/flags-react'
import { getVisitorId } from './lib/visitor'
import App from './App'

const visitorId = getVisitorId()

// Read stored attributes if available — adjust to your auth/storage layer
const storedPlan = localStorage.getItem('sk_plan') ?? 'free'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <SignaKitProvider
      sdkKey="sk_browser_yourOrgId_yourProjectId_random"
      userId={visitorId}
      attributes={{ plan: storedPlan }}
      loadingFallback={<div>Loading…</div>}
    >
      <App />
    </SignaKitProvider>
  </StrictMode>
)

Evaluating flags

useFlag hook

useFlag evaluates a single flag for the current user. Always check loading before branching on enabled — the provider needs one async round-trip to fetch your project config.

src/components/Checkout.tsx
import { useFlag } from '@signakit/flags-react'
import { NewCheckout } from './NewCheckout'
import { LegacyCheckout } from './LegacyCheckout'
import { CheckoutSkeleton } from './CheckoutSkeleton'

export function Checkout() {
  const { enabled, variationKey, loading } = useFlag('checkout-redesign')

  if (loading) return <CheckoutSkeleton />

  return enabled ? <NewCheckout /> : <LegacyCheckout />
}

useFlag returns:

FieldTypeDescription
enabledbooleantrue when the flag is on for this user. Always false while loading.
variationKeystringAssigned variation key ('on', 'off', 'treatment', etc.). 'off' while loading.
ruleKeystring | nullMatched rule key, or null.
ruleTypestring | nullRule type that produced the decision ('experiment', 'rollout', 'targeted', etc.).
variablesRecord<string, VariableValue>Resolved variable values for the matched variation.
loadingbooleantrue until the provider finishes fetching config.

FlagGate component

Use FlagGate when you want declarative show/hide without writing a loading branch yourself. Children render only after loading is complete and the flag is enabled; the fallback renders while loading or when the flag is off.

src/components/Dashboard.tsx
import { FlagGate } from '@signakit/flags-react'
import { BetaAnalytics } from './BetaAnalytics'
import { NewSidebar } from './NewSidebar'

export function Dashboard() {
  return (
    <div>
      <FlagGate flag="new-sidebar">
        <NewSidebar />
      </FlagGate>

      <FlagGate flag="beta-analytics" fallback={<p>Analytics coming soon.</p>}>
        <BetaAnalytics />
      </FlagGate>
    </div>
  )
}

FlagGate props:

PropTypeRequiredDescription
flagstringFlag key to evaluate
fallbackReactNodeRendered while loading or when flag is off

Reading variables

When your flag has variable values (e.g. a banner colour or copy string), read them from variables in the useFlag result.

src/components/Banner.tsx
import { useFlag } from '@signakit/flags-react'

export function PromoBanner() {
  const { enabled, variables, loading } = useFlag('promo-banner')

  if (loading || !enabled) return null

  const headline = typeof variables.headline === 'string'
    ? variables.headline
    : 'Limited time offer'

  return <div className="banner">{headline}</div>
}

Updating userId after login

If a visitor logs in after the app loads, update userId on the provider. Changing userId triggers re-evaluation of all flags automatically.

src/App.tsx
import { useState } from 'react'
import { SignaKitProvider } from '@signakit/flags-react'
import { getVisitorId } from './lib/visitor'

export default function App() {
  // Start with the anonymous visitor ID; swap to the real user ID after login
  const [userId, setUserId] = useState(getVisitorId)

  function handleLogin(user: { id: string }) {
    setUserId(user.id)
  }

  return (
    <SignaKitProvider
      sdkKey="sk_browser_yourOrgId_yourProjectId_random"
      userId={userId}
    >
      <Router onLogin={handleLogin} />
    </SignaKitProvider>
  )
}

When the same user is seen anonymously and then logs in, SignaKit uses the userId at the time of each event. If you want to stitch the anonymous and authenticated sessions together, alias the visitor ID to the real user ID in your analytics pipeline — SignaKit does not perform cross-device identity resolution itself.


Memoizing attributes

SignaKitProvider re-evaluates all flags when attributes changes reference. If you construct the attributes object inside a component render, wrap it in useMemo to keep the reference stable.

src/App.tsx
import { useMemo } from 'react'
import { SignaKitProvider } from '@signakit/flags-react'

export default function App() {
  const user = useCurrentUser() // your auth hook

  // ✓ Stable reference — only updates when plan or country change
  const attributes = useMemo(
    () => ({ plan: user?.plan ?? 'free', country: user?.country ?? 'US' }),
    [user?.plan, user?.country]
  )

  return (
    <SignaKitProvider
      sdkKey="sk_browser_yourOrgId_yourProjectId_random"
      userId={user?.id ?? getVisitorId()}
      attributes={attributes}
    >
      <App />
    </SignaKitProvider>
  )
}

Passing an inline object (attributes={{ plan: 'pro' }}) directly on the provider creates a new object on every parent render and re-evaluates every flag each time. Always memoize.


Tracking conversion events

Use useSignaKitContext to get the current userContext, then call trackEvent on it. Wrap this in a small custom hook so components don't need to know about the context shape.

src/lib/use-track.ts
import { useSignaKitContext } from '@signakit/flags-react'

export function useTrack() {
  const { userContext } = useSignaKitContext()

  return function track(eventName: string, properties?: Record<string, unknown>) {
    if (!userContext) return
    userContext.trackEvent(eventName, properties)
  }
}
src/components/PurchaseButton.tsx
import { useTrack } from '../lib/use-track'

export function PurchaseButton({ amount }: { amount: number }) {
  const track = useTrack()

  function handleClick() {
    // ... process purchase
    track('purchase_completed', { value: amount })
  }

  return <button onClick={handleClick}>Complete purchase</button>
}

trackEvent is fire-and-forget and never throws. If the provider hasn't finished initializing when the event fires, userContext will be null and the call is silently skipped — no error handling needed.


Handling config fetch failures

By default, if the config fetch fails SignaKitProvider fails open: children render, and every useFlag call returns { enabled: false, loading: false }. To handle failures explicitly, pass onError:

src/main.tsx
<SignaKitProvider
  sdkKey="sk_browser_yourOrgId_yourProjectId_random"
  userId={visitorId}
  onError={(reason) => {
    console.warn('[flags] config fetch failed:', reason)
    // optional: send to your error tracker
  }}
>
  <App />
</SignaKitProvider>

Your app keeps working — all flags fall back to off. The onError callback is informational only.


Anti-patterns

PatternProblemFix
Using sk_prod_ key in a SPAServer key exposed in the browser bundleUse a browser SDK key from project settings
Inline attributes object on the providerNew object on every render re-evaluates all flagsMemoize with useMemo
Rendering flag-gated UI before loading: falseFlashes the wrong variant during config fetchCheck loading or use loadingFallback on the provider
Calling getVisitorId() inside a componentGenerates a new ID on every render if localStorage is missingCall it once outside the component tree (in main.tsx)
Changing userId on every renderTriggers full re-evaluation on every renderDerive userId from stable state or useMemo
Using the React SDK in a Next.js server componentHooks and browser APIs cannot run server-sideUse @signakit/flags-node in server components

Last updated on

On this page