React Native integration
The @avsbhq/react-native package brings A vs B feature flags to iOS and Android. It runs on React Native 0.74+ (New Architecture supported) and the Hermes engine. The package mirrors the @avsbhq/react hook API so code is portable between web and mobile. Key differences from the browser SDK: persistent storage uses AsyncStorage instead of localStorage, and datafile updates use long-polling instead of EventSource (which is not available on all React Native versions).
Install
1npm install @avsbhq/react-native @react-native-async-storage/async-storageLink the native module if you are not using Expo (Expo Managed Workflow links it automatically):
1# React Native CLI2npx pod-installObtain your SDK key
Open your A vs B project, go to Settings → Environments, and copy the Client SDK key for your mobile environment. Client SDK keys are safe to embed in mobile app bundles — they only allow flag evaluation and event tracking, not dashboard access.
Create the AsyncStorage adapter
The SDK uses a UnifiedStorageAdapter interface for both sticky bucketing and datafile caching. The @avsbhq/react-native package exports a ready-made adapter wired to @react-native-async-storage/async-storage:
1import AsyncStorage from '@react-native-async-storage/async-storage'2import { createAsyncStorageAdapter } from '@avsbhq/react-native'3
4export const asyncStorageAdapter = createAsyncStorageAdapter(AsyncStorage)Wrap your app with AvsbProvider
Place AvsbProvider near the root of your component tree. Pass the asyncStorageAdapter so the SDK can persist the datafile and sticky assignments across app restarts. Use the bootstrap prop to pass a pre-fetched datafile on cold starts and eliminate any loading flash.
1import React from 'react'2import { AvsbProvider } from '@avsbhq/react-native'3import { asyncStorageAdapter } from './avsb'4
5// Optional: pre-fetch datafile server-side and pass as bootstrap6// to avoid a loading state on first open.7const bootstrapDatafile = undefined // or fetch from your API at startup8
9export default function App() {10 return (11 <AvsbProvider12 sdkKey="sdk_production_..."13 storage={asyncStorageAdapter}14 bootstrap={bootstrapDatafile}15 context={{ kind: 'user', key: 'anon' }}16 >17 <RootNavigator />18 </AvsbProvider>19 )20}AvsbProvider for React Native uses a long-poll strategy to receive datafile updates. The default poll interval is 60 seconds. Pass pollingInterval={30_000} to the provider to increase update frequency at the cost of slightly higher battery and network usage.Identify the user
Call useIdentify after login to associate the session with a real user key. The hook re-evaluates all flags for the new context atomically.
1import { useIdentify } from '@avsbhq/react-native'2
3function LoginScreen() {4 const identify = useIdentify()5
6 async function handleLogin(credentials: Credentials) {7 const user = await authService.login(credentials)8
9 // Swap the anonymous context for the authenticated user10 await identify({11 kind: 'user',12 key: user.id,13 plan: user.plan,14 country: user.country,15 })16
17 navigation.navigate('Home')18 }19
20 // ...21}Read a flag
Use the same hooks as @avsbhq/react. They re-render the component whenever the flag value changes (for example after a datafile update or a context change via identify).
1import { useFlag, useFlagValue } from '@avsbhq/react-native'2
3function CheckoutScreen() {4 // Full Flag<T> object — gives access to source, variationKey, reasons5 const checkout = useFlag('checkout-v2', false)6
7 // Shorthand — just the value8 const theme = useFlagValue('ui-theme', 'default')9
10 if (checkout.value) {11 return <NewCheckout theme={theme} />12 }13 return <LegacyCheckout />14}Track an event
1import { useTrack } from '@avsbhq/react-native'2
3function PurchaseButton({ amount }: { amount: number }) {4 const track = useTrack()5
6 function handlePress() {7 track('purchase', { value: amount, properties: { currency: 'usd' } })8 }9
10 return <Button title="Buy" onPress={handlePress} />11}Bootstrap for fast cold starts
On the very first app open the SDK needs to fetch the datafile before flags are available. To avoid a loading state, pre-fetch the datafile from your own API at app launch and pass it as the bootstrap prop. Subsequent opens will use the cached version from AsyncStorage.
1import React, { useEffect, useState } from 'react'2import { AvsbProvider } from '@avsbhq/react-native'3import type { FlagDatafile } from '@avsbhq/react-native'4import { asyncStorageAdapter } from './avsb'5
6export default function App() {7 const [datafile, setDatafile] = useState<FlagDatafile | undefined>()8
9 useEffect(() => {10 // Fetch from your own API which proxies the A vs B CDN11 fetch('/api/flags/bootstrap')12 .then(r => r.json<FlagDatafile>())13 .then(setDatafile)14 .catch(() => { /* non-fatal — SDK falls back to its own fetch */ })15 }, [])16
17 return (18 <AvsbProvider19 sdkKey="sdk_production_..."20 storage={asyncStorageAdapter}21 bootstrap={datafile}22 context={{ kind: 'user', key: 'anon' }}23 >24 <RootNavigator />25 </AvsbProvider>26 )27}Flush on background / close
React Native apps suspend rather than terminate. Use the AppState API to flush pending events when the app moves to the background:
1import { AppState } from 'react-native'2import { useAvsbClient } from '@avsbhq/react-native'3
4function FlushOnBackground() {5 const client = useAvsbClient()6
7 useEffect(() => {8 const sub = AppState.addEventListener('change', state => {9 if (state === 'background' || state === 'inactive') {10 // Non-blocking — React Native keeps the JS thread alive briefly11 void client.flush()12 }13 })14 return () => sub.remove()15 }, [client])16
17 return null18}Testing
Use @avsbhq/test and the AvsbTestProvider to test components that read flags without a real network connection.
1import React from 'react'2import { render, screen } from '@testing-library/react-native'3import { AvsbTestProvider } from '@avsbhq/test'4import { TestData } from '@avsbhq/test'5import CheckoutScreen from './CheckoutScreen'6
7const td = TestData.flag('checkout-v2')8 .booleanFlag()9 .variationForUser('u_1', true)10 .fallthroughVariation(false)11
12describe('CheckoutScreen', () => {13 it('renders new checkout when flag is on', () => {14 render(15 <AvsbTestProvider flags={[td.build()]} context={{ kind: 'user', key: 'u_1' }}>16 <CheckoutScreen />17 </AvsbTestProvider>18 )19 expect(screen.getByTestId('new-checkout')).toBeTruthy()20 })21
22 it('renders legacy checkout by default', () => {23 render(24 <AvsbTestProvider flags={[td.build()]} context={{ kind: 'user', key: 'other' }}>25 <CheckoutScreen />26 </AvsbTestProvider>27 )28 expect(screen.getByTestId('legacy-checkout')).toBeTruthy()29 })30})What's next
- React Native SDK reference — full hook and provider API.
- Multi-context targeting — combine user and device contexts (useful for targeting by OS version or device type).
- Sticky bucketing — guarantee that users see the same variation across app restarts using the AsyncStorage backend.
- Next.js App Router integration — if you also run a Next.js web app, share the same flags across platforms.