@avsbhq/react-native
v1.0.0
Published
React Native SDK for the [A vs B](https://app.avsb.cloud) platform.
Readme
@avsbhq/react-native
React Native SDK for the A vs B platform.
Feature flags and A/B experiments for iOS and Android apps. Built on @avsbhq/browser and @avsbhq/react with React Native-specific adaptations: AsyncStorage-backed datafile caching and sticky bucketing, no dependency on localStorage or EventSource, and support for the Hermes JIT engine.
1. Install
npm install @avsbhq/react-native @react-native-async-storage/async-storageThen link the native module:
cd ios && pod installPeer dependencies:
react-native>= 0.74 (New Architecture compatible)react>= 18@react-native-async-storage/async-storage>= 1.18 (optional but recommended for persistence)
2. Quickstart
// App.tsx
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AvsbProvider } from '@avsbhq/react-native'
export function App() {
return (
<AvsbProvider
sdkKey="sdk-client-..."
context={{ kind: 'user', key: userId, plan: userPlan }}
storage={AsyncStorage}
>
<MainNavigator />
</AvsbProvider>
)
}// CheckoutButton.tsx
import { useBoolFlag, useTrack } from '@avsbhq/react-native'
export function CheckoutButton() {
const flag = useBoolFlag('new-checkout-flow', false)
const track = useTrack()
return (
<Pressable onPress={() => track('checkout_tapped')}>
<Text>{flag.isEnabled() ? 'New Checkout' : 'Checkout'}</Text>
</Pressable>
)
}3. SDK keys
Get a client SDK key from your A vs B project's Environments page. Client keys are safe to embed in your app bundle. Store them in your build config or .env files — not hardcoded in source:
AVSB_SDK_KEY=sdk-client-...import Config from 'react-native-config'
const sdkKey = Config.AVSB_SDK_KEYNever use a server SDK key (sdk-server-...) in a React Native app — server keys grant broader API access and must stay server-side.
4. Identity
Provider-level context
Pass a context to <AvsbProvider> at startup:
<AvsbProvider
sdkKey={sdkKey}
context={{ kind: 'user', key: userId, plan: 'pro' }}
>useIdentify
Replace the context after sign-in:
import { useIdentify } from '@avsbhq/react-native'
const identify = useIdentify()
async function onSignIn(user: User) {
identify({ kind: 'user', key: user.id, email: user.email, plan: user.plan })
}useAlias
Link an anonymous device ID to an identified user on sign-in:
import { useAlias } from '@avsbhq/react-native'
const alias = useAlias()
async function onSignIn(deviceId: string, user: User) {
await alias(
{ kind: 'user', key: deviceId },
{ kind: 'user', key: user.id }
)
identify({ kind: 'user', key: user.id, plan: user.plan })
}useReset
Return to an anonymous context on sign-out:
import { useReset } from '@avsbhq/react-native'
const reset = useReset()
function onSignOut() { reset() }5. Multi-context
identify({
kind: 'multi',
user: { kind: 'user', key: userId, plan: 'pro' },
device: { kind: 'device', key: deviceId, os: Platform.OS },
organization: { kind: 'organization', key: orgId, tier: 'enterprise' },
})6. Reading flags
All hooks from @avsbhq/react are re-exported and work identically in React Native:
import { useBoolFlag, useStringFlag, useNumberFlag, useJsonFlag, useFlag, useFlagValue, useAllFlags } from '@avsbhq/react-native'
const darkMode = useBoolFlag('dark-mode', false)
const theme = useStringFlag('theme', 'light')
const maxItems = useNumberFlag('max-results', 25)
const config = useJsonFlag<{ retryCount: number }>('api-config', { retryCount: 3 })All return Flag<T>:
flag.value // T
flag.isEnabled() // true if source === 'rule' && value is truthy
flag.variationKey // 'on' | 'off' | null
flag.source // 'rule' | 'sticky' | 'default' | ...
flag.reasons // string[]7. Tracking events
const track = useTrack()
track('screen_viewed', {
value: 1,
properties: { screen: 'checkout', os: Platform.OS },
})Events are queued and flushed in batches. The flush uses fetch with keepalive: true where available; falls back to a regular fetch call on app background.
8. Error handling
<AvsbProvider
sdkKey={sdkKey}
context={ctx}
onError={(err, source) => {
crashlytics().recordError(err, source)
}}
>source is one of 'init' | 'poll' | 'track' | 'eval'.
For structured logging:
import { createLogger, consoleTransport } from '@avsbhq/browser'
<AvsbProvider
sdkKey={sdkKey}
context={ctx}
logger={createLogger({
level: 'warn',
transports: [consoleTransport({ level: 'warn' })],
})}
>9. SSR / hydration
Not applicable to React Native. There is no server-side rendering; the app renders natively on the device.
For reducing cold-start latency, use the storage prop to cache the datafile in AsyncStorage. On subsequent app launches, the cached datafile is loaded synchronously before the first network poll:
import AsyncStorage from '@react-native-async-storage/async-storage'
<AvsbProvider
sdkKey={sdkKey}
context={ctx}
storage={AsyncStorage}
>To pre-warm the in-memory sticky bucket cache from persisted storage on app boot:
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createAsyncStorageAdapter, warmFromStorage } from '@avsbhq/react-native'
const adapter = createAsyncStorageAdapter({ storage: AsyncStorage })
await warmFromStorage(adapter) // call before mounting the provider10. Graceful shutdown
React Native apps are backgrounded rather than terminated. Flush events when the app moves to the background using AppState:
import { AppState } from 'react-native'
import { useAvsbClient } from '@avsbhq/react-native'
const client = useAvsbClient()
useEffect(() => {
const sub = AppState.addEventListener('change', (state) => {
if (state === 'background' || state === 'inactive') {
void client?.flush()
}
})
return () => sub.remove()
}, [client])When the app is unmounted (unusual but possible in tests or hot reload):
useEffect(() => {
return () => { void client?.close() }
}, [client])11. Testing
import { render, screen } from '@testing-library/react-native'
import { AvsbTestProvider } from '@avsbhq/test'
test('shows new checkout when enabled', () => {
render(
<AvsbTestProvider mockFlags={{ 'new-checkout-flow': true }}>
<CheckoutButton />
</AvsbTestProvider>
)
expect(screen.getByText('New Checkout')).toBeTruthy()
})For fluent per-user control:
import { TestData, createMockClient } from '@avsbhq/test'
const td = TestData.flag('new-checkout-flow')
.booleanFlag()
.variationForUser('u_paid', true)
.fallthroughVariation(false)
const mockClient = createMockClient({ flags: [td.build()] })12. Migration
From LaunchDarkly React Native
| LaunchDarkly React Native | @avsbhq/react-native |
|---|---|
| <LDProvider mobileKey="..." context={...}> | <AvsbProvider sdkKey="..." context={...}> |
| useLDClient() | useAvsbClient() |
| useFlags() | useAllFlags() |
| useLDFlag('key', default) | useFlag('key', default).value |
| ldClient.identify(context) | useIdentify()(context) |
| ldClient.track('event') | useTrack()('event') |
| ldClient.flush() | void client?.flush() |
Key differences:
useFlagreturns aFlag<T>object. Access.valuefor the primitive or useuseFlagValue.- AsyncStorage is optional — pass it as
storage={AsyncStorage}for datafile caching. No wiring code needed. - Multi-context is native — no
LDMultiKindContextadapter required. - No EventSource — streaming is unavailable in React Native (polling is used instead).
From Statsig React Native
| Statsig React Native | @avsbhq/react-native |
|---|---|
| <StatsigProvider sdkKey="..." user={...}> | <AvsbProvider sdkKey="..." context={...}> |
| 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 }) |
