@avsbhq/react
v1.0.0
Published
React SDK for the [A vs B](https://app.avsb.cloud) platform.
Downloads
262
Readme
@avsbhq/react
React SDK for the A vs B platform.
Wrap your component tree with <AvsbProvider> and read feature flags anywhere with hooks. Built on @avsbhq/browser; components re-render automatically when flag values change. Every hook is backed by useSyncExternalStore for tear-free concurrent mode updates.
1. Install
npm install @avsbhq/reactReact 18 or later is required as a peer dependency. @avsbhq/browser ships as a runtime dependency — you do not need to install it separately.
2. Quickstart
// app.tsx — or your root layout
import { AvsbProvider } from '@avsbhq/react'
export function App({ children }: { children: React.ReactNode }) {
return (
<AvsbProvider
sdkKey={process.env.NEXT_PUBLIC_AVSB_SDK_KEY!}
context={{ kind: 'user', key: userId, plan: userPlan }}
>
{children}
</AvsbProvider>
)
}// CheckoutButton.tsx
import { useBoolFlag, useTrack } from '@avsbhq/react'
export function CheckoutButton() {
const newCheckout = useBoolFlag('new-checkout-flow', false)
const track = useTrack()
return (
<button onClick={() => track('checkout_clicked', { properties: { location: 'header' } })}>
{newCheckout.isEnabled() ? 'Start checkout (new)' : 'Buy now'}
</button>
)
}3. SDK keys
Get a client SDK key from your A vs B project's Environments page:
NEXT_PUBLIC_AVSB_SDK_KEY=sdk-client-...
VITE_AVSB_SDK_KEY=sdk-client-...Client SDK keys are safe in browser bundles. Never use a server SDK key (sdk-server-...) with @avsbhq/react.
See @avsbhq/browser documentation for key rotation.
4. Identity
Provider-level context
Pass a context object to <AvsbProvider> at bootstrap. It is the evaluation context used for all flag reads until you call identify:
<AvsbProvider
sdkKey={sdkKey}
context={{ kind: 'user', key: userId, email: userEmail, plan: 'pro' }}
>When context changes between renders, <AvsbProvider> calls client.identify() automatically and re-evaluates all flags.
useIdentify
Replace the entire context at runtime (e.g. after sign-in):
import { useIdentify } from '@avsbhq/react'
const identify = useIdentify()
function onSignIn(user: User) {
identify({ kind: 'user', key: user.id, email: user.email, plan: user.plan })
}The returned function is referentially stable across renders.
useAlias
Link an anonymous visitor to an identified user. Call this once at sign-in:
import { useAlias } from '@avsbhq/react'
const alias = useAlias()
async function onSignIn(anonKey: string, user: User) {
await alias(
{ kind: 'user', key: anonKey },
{ kind: 'user', key: user.id }
)
identify({ kind: 'user', key: user.id, plan: user.plan })
}useReset
Return to an anonymous context on logout:
import { useReset } from '@avsbhq/react'
const reset = useReset()
function onSignOut() { reset() }5. Multi-context
identify({
kind: 'multi',
user: { kind: 'user', key: 'u_123', plan: 'pro' },
organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },
})After calling identify with a multi-context, all hooks evaluate against every context kind simultaneously.
6. Reading flags
Typed hooks
Use the typed hooks to get a runtime type-safety check and the full Flag<T> object:
import { useBoolFlag, useStringFlag, useNumberFlag, useJsonFlag } from '@avsbhq/react'
const darkMode = useBoolFlag('dark-mode', false)
const theme = useStringFlag('theme', 'light')
const maxItems = useNumberFlag('max-results', 25)
const config = useJsonFlag<{ timeout: number }>('api-config', { timeout: 5000 })All return Flag<T>. A defaultValue is always required.
useFlag<T> — generic
import { useFlag } from '@avsbhq/react'
const pricing = useFlag<{ tiers: number }>('pricing-config', { tiers: 3 })useFlagValue<T> — raw value shortcut
Returns T directly when you only need the value and not the metadata:
import { useFlagValue } from '@avsbhq/react'
const showBanner = useFlagValue('promo-banner', false)
// showBanner is booleanThe Flag<T> object
flag.value // T — the variation value
flag.isEnabled() // true if source === 'rule' && value is truthy
flag.variationKey // 'on' | 'off' | 'control' | null
flag.exists() // false when source === 'not_found'
flag.source // 'rule' | 'sticky' | 'holdout' | 'default' | ...
flag.ruleId // which rule matched
flag.reasons // string[] — evaluation pathVariant rendering
Use variationKey when you need to branch on a specific variation identity rather than a boolean gate:
import { useStringFlag } from '@avsbhq/react'
function PricingPage() {
const pricingExp = useStringFlag('pricing-experiment', 'control')
if (pricingExp.variationKey === 'three-tier') return <ThreeTierPricing />
if (pricingExp.variationKey === 'usage-based') return <UsageBasedPricing />
return <DefaultPricing />
}useAllFlags
import { useAllFlags } from '@avsbhq/react'
const flags = useAllFlags()
// Record<string, Flag> — re-renders on any flagChange eventPrefer individual typed hooks in components. useAllFlags is suited for debug panels or bulk snapshot use.
Suspense
import { useFlagSuspense } from '@avsbhq/react'
function CheckoutFeature() {
const flag = useFlagSuspense('checkout-v2', false)
return flag.isEnabled() ? <CheckoutV2 /> : <Checkout />
}
// In parent:
<Suspense fallback={<Skeleton />}>
<CheckoutFeature />
</Suspense>useFlagSuspense throws the client.onReady() Promise while the SDK is loading, then resolves with the flag value.
Status hook
import { useAvsbStatus } from '@avsbhq/react'
const { status, error } = useAvsbStatus()
// status: 'loading' | 'ready' | 'error'7. Tracking events
import { useTrack } from '@avsbhq/react'
const track = useTrack()
function handlePurchase() {
track('purchase_completed', {
value: 199.0,
properties: { plan: 'annual' },
})
}useTrack returns a stable function that does not change reference across renders.
8. Error handling
Provider-level error state
Use useFlagReady to gate rendering until the SDK is ready:
import { useFlagReady } from '@avsbhq/react'
function App() {
const ready = useFlagReady()
if (!ready) return <Skeleton />
return <MainContent />
}Custom logger
import { createLogger, consoleTransport } from '@avsbhq/browser'
<AvsbProvider
sdkKey={sdkKey}
context={ctx}
logger={createLogger({
level: 'warn',
transports: [consoleTransport({ level: 'warn' })],
})}
>onError callback
<AvsbProvider
sdkKey={sdkKey}
context={ctx}
onError={(err, source) => {
Sentry.captureException(err, { tags: { sdkSource: source } })
}}
>Direct client access
import { useAvsbClient } from '@avsbhq/react'
const client = useAvsbClient()
React.useEffect(() => {
if (!client) return
return client.on('error', (err) => {
myLogger.error('SDK error', { error: err.message })
})
}, [client])9. SSR / hydration
Pass a bootstrap datafile to skip the initial network round-trip on the client and ensure the server render matches the hydration output:
// Server: fetch the datafile and pass it as a prop or window variable
<AvsbProvider
sdkKey={sdkKey}
context={ctx}
bootstrap={bootstrapDatafileFromServer}
>// Server-side: generate the bootstrap blob
const serverClient = new AvsbClient({ sdkKey: serverKey, context: ctx })
await serverClient.onReady()
const bootstrap = await serverClient.utils.serializeBootstrap(ctx)
// serialize to JSON and embed in the HTML responseFor Next.js App Router, use @avsbhq/next instead — it provides AvsbHydrator and AvsbServerProvider which handle the server-to-client bootstrap automatically.
Flag hooks return a not_found sentinel during the hydration window so the SSR output matches the initial client render and avoids React hydration mismatches.
Pre-built client injection (Mode B)
When you own the SDK client lifecycle:
const preBuiltClient = new AvsbClient({ sdkKey, context })
await preBuiltClient.onReady()
<AvsbProvider client={preBuiltClient}>
{children}
</AvsbProvider>When client is provided, <AvsbProvider> does not call client.close() on unmount — you are responsible for teardown.
10. Graceful shutdown
When using Mode A (sdkKey prop), <AvsbProvider> automatically closes the client on unmount and flushes queued events.
When using Mode B (pre-built client):
useEffect(() => {
return () => { void client.close() } // flush + teardown
}, [client])For tab close in any mode:
useEffect(() => {
const handler = () => { void client?.flush() }
window.addEventListener('beforeunload', handler)
return () => window.removeEventListener('beforeunload', handler)
}, [client])11. Testing
import { render, screen } from '@testing-library/react'
import { AvsbTestProvider } from '@avsbhq/test'
import { CheckoutButton } from './CheckoutButton'
test('shows new checkout when flag is enabled', () => {
render(
<AvsbTestProvider mockFlags={{ 'new-checkout-flow': true }}>
<CheckoutButton />
</AvsbTestProvider>
)
expect(screen.getByRole('button')).toHaveTextContent('Start checkout (new)')
})
test('shows default when flag is off', () => {
render(
<AvsbTestProvider mockFlags={{ 'new-checkout-flow': false }}>
<CheckoutButton />
</AvsbTestProvider>
)
expect(screen.getByRole('button')).toHaveTextContent('Buy now')
})For fluent per-user control:
import { TestData, createMockClient } from '@avsbhq/test'
const td = TestData.flag('checkout-v2')
.booleanFlag()
.variationForUser('u_paying', true)
.fallthroughVariation(false)
const client = createMockClient({ flags: [td.build()] })For non-rendering subscriptions:
import { useFlagSubscription } from '@avsbhq/react'
// In your component test — emits callbacks without triggering re-render
useFlagSubscription('checkout-v2', (flag) => {
myAnalytics.record(flag.variationKey)
})12. Migration
From LaunchDarkly React
| LaunchDarkly React | @avsbhq/react |
|---|---|
| <LDProvider clientSideID="..."> | <AvsbProvider sdkKey="..."> |
| useLDClient() | useAvsbClient() |
| useFlags() | useAllFlags() |
| useLDFlag('key', default) | useFlag('key', default).value |
| ldClient.identify(context) | useIdentify()(context) |
| ldClient.track('event') | useTrack()('event') |
| withLDProvider(options)(App) | <AvsbProvider ...> — no HOC needed |
Key differences:
useFlagreturns aFlag<T>object, not a rawT. Access.valuefor the primitive or useuseFlagValuefor a convenience shortcut.- All hooks require an explicit
defaultValue. - Multi-context is native — pass
{ kind: 'multi', user: ..., organization: ... }to<AvsbProvider context={...}>. useFlagDetailsis gone — the unifiedFlag<T>fromuseFlagcarries the same metadata (variationKey,source,reasons,ruleId).
From Statsig React
| Statsig React | @avsbhq/react |
|---|---|
| <StatsigProvider sdkKey="..."> | <AvsbProvider sdkKey="..."> |
| useGate('gate') | useBoolFlag('gate', false).isEnabled() |
| useExperiment('exp').get('param', default) | useFlag('exp', default).value |
| useStatsigClient() | useAvsbClient() |
| logEvent('event', value, metadata) | useTrack()('event', { value, properties: metadata }) |
