Vite + React
@avsbhq/react is all a Vite single-page app needs. Wrap the tree in AvsbProvider, read flags with hooks, and a component re-renders when the flag it reads changes, and only then. No server rendering is involved.
This page assumes React 18 or later.
Install
One package. @avsbhq/browser ships as a real dependency, so there is nothing else to install.
Add your SDK key
Open Environments in your A vs B project sidebar. Vite only exposes variables prefixed VITE_ to the bundle.
Wrap your app
Mount AvsbProvider at the root. Leave context off until you know who the visitor is.
Read flags and record exposures
Reads never fire exposures. useExposure is what records that the visitor saw the variation.
Identify after sign-in
Call identify from an effect or an event handler, never during render.
Install
npm install @avsbhq/reactAdd your SDK key
# .envVITE_AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xnWrap your app
// src/main.tsximport React from 'react'import ReactDOM from 'react-dom/client'import { AvsbProvider } from '@avsbhq/react'import { App } from './App'const root = document.getElementById('root')if (!root) throw new Error('#root is missing from index.html')ReactDOM.createRoot(root).render( <React.StrictMode> <AvsbProvider sdkKey={import.meta.env.VITE_AVSB_SDK_KEY as string}> <App /> </AvsbProvider> </React.StrictMode>,)Do not pass a shared placeholder such as context={{ kind: 'user', key: 'anonymous' }}. Every visitor would hash to the same bucket, so an experiment would send all of them to one variation. With no context, the SDK keeps a per-visitor anonymous id of its own, which is what makes a 50/50 split a 50/50 split.
The provider builds the client in a layout effect and closes it on unmount, so React StrictMode's double-invoked effects get a fresh live client rather than a closed one. Passing a pre-built client prop instead puts its lifecycle back in your hands.
Read a flag
// src/components/CheckoutPage.tsximport { useBoolFlag, useExposure } from '@avsbhq/react'import type { Flag } from '@avsbhq/react'export function CheckoutPage() { const checkoutV2: Flag<boolean> = useBoolFlag('checkout_v2', false) useExposure('checkout_v2') return checkoutV2.isEnabled() ? <NewCheckout /> : <LegacyCheckout />}Every read returns a Flag<T> object, never a bare value, and always takes a default. The default is what you get before the SDK is ready, when the key does not exist, and when the flag's declared type contradicts the getter you called (a type mismatch warns once and never throws).
The typed getters are useBoolFlag, useStringFlag, useNumberFlag and useJsonFlag<T>; useFlag<T> skips the runtime type check, and useFlagValue<T> returns the bare value when you do not need the metadata.
Each subscription is independent: only components reading a flag that actually changed re-render, whether the change came from a poll or from identify.
Exposures
// src/components/PromoBanner.tsximport { useBoolFlag, useExposure } from '@avsbhq/react'export function PromoBanner({ isVisible }: { isVisible: boolean }) { const promo = useBoolFlag('summer_promo', false) useExposure('summer_promo', { enabled: isVisible }) if (!isVisible || !promo.isEnabled()) return null return <Banner />}Rendering hooks read a cached, side-effect-free snapshot, so React can retry, discard, and replay renders without touching your results. useExposure fires once per flag per mounted view, after flags are readable, so an exposure is never recorded against a default value. Hold it back until the variation is really on screen with { enabled }.
Track a conversion
// src/components/PricingCard.tsximport { useTrack } from '@avsbhq/react'export function PricingCard({ plan }: { plan: string }) { const track = useTrack() function handleUpgrade(): void { track('plan_upgrade', { value: 49.99, properties: { plan, source: 'pricing_page' } }) } return ( <button type="button" onClick={handleUpgrade}> Upgrade to {plan} </button> )}Identify after sign-in
// src/hooks/usePostLogin.tsimport { useEffect } from 'react'import { useAlias, useIdentify } from '@avsbhq/react'export function usePostLogin(userId: string | null, plan: string): void { const identify = useIdentify() const alias = useAlias() useEffect(() => { if (!userId) return void alias({ kind: 'user', key: 'anon-session' }, { kind: 'user', key: userId }).then(() => { identify({ kind: 'user', key: userId, plan }) }) }, [alias, identify, userId, plan])}identify replaces the bound context and re-buckets every flag, which is why it belongs in an effect and not in a render. alias first stitches the anonymous session to the identified one, so the events from before sign-in still count. useReset() rotates to a new anonymous identity on sign-out, so the next visitor on that browser is genuinely a different person.
Multi-context targeting
// src/lib/identifyOrg.tsimport type { EvalContext } from '@avsbhq/react'export const organizationContext: EvalContext = { kind: 'multi', user: { kind: 'user', key: 'u_123', plan: 'pro' }, organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },}A rule can then bucket on user.key while matching an audience condition on organization.tier.
Loading and failure states
// src/components/Gate.tsximport { useAvsbStatus, useFlagReady } from '@avsbhq/react'import type { ReactNode } from 'react'export function Gate({ children }: { children: ReactNode }) { const ready = useFlagReady() const { status, error, degraded } = useAvsbStatus() if (status === 'error') return <ErrorBanner message={error?.message} /> if (!ready) return <Skeleton /> return ( <> {degraded ? <StaleFlagsNotice /> : null} {children} </> )}degraded means a cached datafile is being served because a refresh failed: flags answer, and they may be stale. A degraded SDK is never reported as an error.
Testing
@avsbhq/react/testing turns a flags map into a real datafile, bootstraps a real client from it, and renders the real provider, so a test exercises the same code path production does. Nothing touches the network.
// src/components/CheckoutPage.test.tsximport { render, screen } from '@testing-library/react'import { AvsbTestProvider } from '@avsbhq/react/testing'import { CheckoutPage } from './CheckoutPage'test('renders the new checkout when the flag is on', () => { render( <AvsbTestProvider flags={{ 'checkout_v2': true }}> <CheckoutPage /> </AvsbTestProvider>, ) expect(screen.getByTestId('new-checkout')).toBeInTheDocument()})Need per-user variations, audiences, or a rollout in a test? Build the datafile with createTestDatafile and pass it as the datafile prop.
What's next
- Multi-context identity: target users by organization, device, or your own context kinds.
- Sticky bucketing: keep an assignment stable across sessions.
- SDK installation: the hook surface,
Flag<T>, and every evaluation source.