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.

1

Install

One package. @avsbhq/browser ships as a real dependency, so there is nothing else to install.

2

Add your SDK key

Open Environments in your A vs B project sidebar. Vite only exposes variables prefixed VITE_ to the bundle.

3

Wrap your app

Mount AvsbProvider at the root. Leave context off until you know who the visitor is.

4

Read flags and record exposures

Reads never fire exposures. useExposure is what records that the visitor saw the variation.

5

Identify after sign-in

Call identify from an effect or an event handler, never during render.

Install

Shell
npm install @avsbhq/react
Shell1 line

Add your SDK key

Shell
# .envVITE_AVSB_SDK_KEY=sdk_production_ttqm0eaj4vth1krcb2xn
Shell2 lines
Your SDK key is public
Your SDK key is a public identifier, not a secret: it is safe to ship in browser and mobile bundles, it can only fetch that environment's flag configuration and send events, and it can never read or change anything in your dashboard. Credentials covers all four A vs B credentials and which one to reach for.

Wrap your app

TypeScript React
// 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>,)
TypeScript React16 lines
Warning

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

TypeScript React
// 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 />}
TypeScript React10 lines

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.

Tip

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

TypeScript React
// 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 />}
TypeScript React10 lines

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

TypeScript React
// 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>  )}
TypeScript React16 lines

Identify after sign-in

TypeScript React
// 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])}
TypeScript React15 lines

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

TypeScript
// 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' },}
TypeScript8 lines

A rule can then bucket on user.key while matching an audience condition on organization.tier.

Loading and failure states

TypeScript React
// 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}    </>  )}
TypeScript React18 lines

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.

TypeScript React
// 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()})
TypeScript React14 lines

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

Was this helpful?