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.

1

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).

2

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.

3

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.

4

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.

5

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.

6

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).

7

Track an event

Use the useTrack hook to record conversion events.

Install the packages:

Shell
npm install @avsbhq/react-native @react-native-async-storage/async-storage
Shell1 line

Link the native module if you are not using Expo:

Shell
# React Native CLInpx pod-install
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.

Create the AsyncStorage adapter (avsb.ts):

TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage'import { createAsyncStorageAdapter } from '@avsbhq/react-native'export const asyncStorageAdapter = createAsyncStorageAdapter({ storage: AsyncStorage })
TypeScript4 lines

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):

TypeScript React
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>  )}
TypeScript React13 lines
Long-poll instead of EventSource

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):

TypeScript React
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')  }  // ...}
TypeScript React24 lines

Read a flag (screens/CheckoutScreen.tsx):

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

Track an event (screens/CheckoutScreen.tsx):

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

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):

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

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):

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

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

Was this helpful?