React Native
The @avsbhq/react-native package brings A vs B feature flags to iOS and Android. A feature flag is a setting in your code that you can turn on, off, or change without shipping a new app update. 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.
Two differences from the browser SDK. First, it checks for datafile updates with long-polling instead of EventSource, because EventSource is not available on all React Native versions. A datafile is the small file of experiment and flag rules the SDK downloads to decide what to show. Second, it ships an AsyncStorage adapter for on-device caching (the browser SDK's equivalent uses localStorage), but that adapter is not wired up to the provider yet, so nothing persists across app restarts today. See Persisting flags across app restarts below for what works now.
Install
Install @avsbhq/react-native and @react-native-async-storage/async-storage. Link the native module if you are not using Expo (Expo Managed Workflow links it automatically).
Obtain your SDK key
Open your A vs B project, go to Environments in the project sidebar, and copy the SDK key for your mobile environment. There is one key per environment, and it is safe to embed in a mobile app bundle.
Create the AsyncStorage adapter
The package exports createAsyncStorageAdapter, which builds a UnifiedStorageAdapter: a small object for reading and writing the datafile and sticky assignments in AsyncStorage. It is real and tested, but AvsbProvider does not use it automatically today.
Wrap your app with AvsbProvider
Place AvsbProvider near the root of your component tree, with your sdkKey and context. From version 1.0.3 every other AvsbClient option, such as pollingInterval, passes straight through to the client, and the provider closes its client when it unmounts.
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.
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).
Track an event
Use the useTrack hook to record conversion events.
Install the packages:
npm install @avsbhq/react-native @react-native-async-storage/async-storageLink the native module if you are not using Expo:
# React Native CLInpx pod-installCreate the AsyncStorage adapter (avsb.ts):
import AsyncStorage from '@react-native-async-storage/async-storage'import { createAsyncStorageAdapter } from '@avsbhq/react-native'export const asyncStorageAdapter = createAsyncStorageAdapter({ storage: AsyncStorage })asyncStorageAdapter is real and tested, but AvsbProvider does not read it yet, so building it here does not make anything persist. See Persisting flags across app restarts below.
Wrap your app with AvsbProvider (App.tsx):
import React from 'react'import { AvsbProvider } from '@avsbhq/react-native'export default function App() { return ( <AvsbProvider sdkKey="sdk_production_..." context={{ kind: 'user', key: 'anon' }} > <RootNavigator /> </AvsbProvider> )}AvsbProvider for React Native uses a long-poll strategy to receive datafile updates. The default poll interval is 60 seconds. There is currently no way to change it through this provider.
Identify the user after login (screens/LoginScreen.tsx):
import { useIdentify } from '@avsbhq/react-native'import { authService } from './authService'import type { Credentials } from './authService'import { navigation } from './navigation'function LoginScreen() { const identify = useIdentify() async function handleLogin(credentials: Credentials) { const user = await authService.login(credentials) // Swap the anonymous context for the authenticated user await identify({ kind: 'user', key: user.id, plan: user.plan, country: user.country, }) navigation.navigate('Home') } // ...}Read a flag (screens/CheckoutScreen.tsx):
import { useFlag, useFlagValue } from '@avsbhq/react-native'function CheckoutScreen() { // Full Flag<T> object: gives access to source, variationKey, reasons const checkout = useFlag('checkout_v2', false) // Shorthand: just the value const theme = useFlagValue('ui_theme', 'default') if (checkout.value) { return <NewCheckout theme={theme} /> } return <LegacyCheckout />}Track an event (screens/CheckoutScreen.tsx):
import { useTrack } from '@avsbhq/react-native'function PurchaseButton({ amount }: { amount: number }) { const track = useTrack() function handlePress() { // `revenue` is money in major units; `value` is the separate // numeric-metric column. track('purchase', { revenue: amount }) } return <Button title="Buy" onPress={handlePress} />}Persisting flags across app restarts
On every cold start, AvsbProvider fetches the datafile fresh over the network. From version 1.0.3 the provider accepts the client's bootstrap option, so an app can start from a datafile it ships with, such as one you bundle at build time. The SDK does not save the datafile it fetches, though, and the storage prop is not connected to anything yet.
createAsyncStorageAdapter (above) is real, exported, and tested. It builds the object the SDK will read once AvsbProvider is wired up to use it. That wiring does not exist yet, so right now there is no supported way to make flags or sticky assignments survive an app restart on React Native.
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 (App.tsx):
import { useEffect } from 'react'import { AppState } from 'react-native'import { useAvsbClient } from '@avsbhq/react-native'function FlushOnBackground() { const client = useAvsbClient() useEffect(() => { const sub = AppState.addEventListener('change', state => { if (state === 'background' || state === 'inactive') { // Non-blocking: React Native keeps the JS thread alive briefly. // The hook returns null until the provider above has a client. void client?.flush() } }) return () => sub.remove() }, [client]) return null}Testing
The React Native hooks are the @avsbhq/react hooks, so the React test provider drives them. AvsbTestProvider from @avsbhq/react/testing turns a flags map into a real datafile, bootstraps a real client from it, and renders the real provider. Nothing touches the network (CheckoutScreen.test.tsx):
import React from 'react'import { render, screen } from '@testing-library/react-native'import { AvsbTestProvider } from '@avsbhq/react/testing'import CheckoutScreen from './CheckoutScreen'test('renders the new checkout when the flag is on', () => { render( <AvsbTestProvider flags={{ 'checkout_v2': true }}> <CheckoutScreen /> </AvsbTestProvider>, ) expect(screen.getByTestId('new-checkout')).toBeTruthy()})test('renders the legacy checkout when it is off', () => { render( <AvsbTestProvider flags={{ 'checkout_v2': false }}> <CheckoutScreen /> </AvsbTestProvider>, ) expect(screen.getByTestId('legacy-checkout')).toBeTruthy()})For per-user variations, build the datafile with createTestDatafile from @avsbhq/react/testing and pass it as the datafile prop. The flags prop is a Record<string, unknown> map, never an array: an earlier release documented flags={[td.build()]}, which does not typecheck and evaluated to defaults at runtime.
What's next
- Multi-context targeting: combine user and device contexts (useful for targeting by OS version or device type).
- Sticky bucketing: what it means to keep a visitor in the same variation. On React Native this does not yet survive an app restart, per Persisting flags across app restarts above.
- Next.js App Router integration: if you also run a Next.js web app, share the same flags across platforms.