/Docs

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

1

Install

terminal
bash
1npm install @avsbhq/react-native @react-native-async-storage/async-storage

Link the native module if you are not using Expo (Expo Managed Workflow links it automatically):

terminal
bash
1# React Native CLI
2npx pod-install
2

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

Use the Client SDK key, not the Server key
Server SDK keys must never be embedded in a mobile app. Always use a Client SDK key for React Native.
3

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:

avsb.ts
typescript
1import AsyncStorage from '@react-native-async-storage/async-storage'
2import { createAsyncStorageAdapter } from '@avsbhq/react-native'
3
4export const asyncStorageAdapter = createAsyncStorageAdapter(AsyncStorage)
4

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.

App.tsx
tsx
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 bootstrap
6// to avoid a loading state on first open.
7const bootstrapDatafile = undefined // or fetch from your API at startup
8
9export default function App() {
10 return (
11 <AvsbProvider
12 sdkKey="sdk_production_..."
13 storage={asyncStorageAdapter}
14 bootstrap={bootstrapDatafile}
15 context={{ kind: 'user', key: 'anon' }}
16 >
17 <RootNavigator />
18 </AvsbProvider>
19 )
20}
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. Pass pollingInterval={30_000} to the provider to increase update frequency at the cost of slightly higher battery and network usage.
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.

screens/LoginScreen.tsx
tsx
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 user
10 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}
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).

screens/CheckoutScreen.tsx
tsx
1import { useFlag, useFlagValue } from '@avsbhq/react-native'
2
3function CheckoutScreen() {
4 // Full Flag<T> object — gives access to source, variationKey, reasons
5 const checkout = useFlag('checkout-v2', false)
6
7 // Shorthand — just the value
8 const theme = useFlagValue('ui-theme', 'default')
9
10 if (checkout.value) {
11 return <NewCheckout theme={theme} />
12 }
13 return <LegacyCheckout />
14}
7

Track an event

screens/CheckoutScreen.tsx
tsx
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.

App.tsx — with server bootstrap
tsx
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 CDN
11 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 <AvsbProvider
19 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:

App.tsx
tsx
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 briefly
11 void client.flush()
12 }
13 })
14 return () => sub.remove()
15 }, [client])
16
17 return null
18}

Testing

Use @avsbhq/test and the AvsbTestProvider to test components that read flags without a real network connection.

CheckoutScreen.test.tsx
tsx
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