React Native SDK
Flags in React Native apps that follow the app's lifecycle, start from the last flags they saw even offline, and come with the React hooks.
@featflags/react-native gives the TypeScript SDK what an app needs: it polls only in the foreground, sends what it counted when the app goes to the background, and keeps the last flags on the device, so a cold start offline still evaluates the flags it last saw. The React provider and hooks come with it. It needs React Native 0.74 or later (Hermes or JSC) and React 18 or later.
Install
npm install @featflags/react-native @react-native-async-storage/async-storage
npm install @react-native-community/netinfo # optional: refetch the moment the network is back
cd ios && pod install
It’s published on npm as @featflags/react-native, and needs AsyncStorage 3 or later.
Apps use the environment’s client key (pk_…). A server key is refused, since its flags carry the secrets of private segments, and anything in an app can be read by its users.
Create a client
createClient reads storage before the client starts, so it’s asynchronous: create the client once, outside React, and hand it to the provider.
// App.tsx
import { createClient, FeatflagsProvider, type FlagClient } from '@featflags/react-native'
import { useEffect, useState } from 'react'
const flags = createClient({
key: 'pk_…',
cdnUrl: 'https://flags.featflags.io',
eventsUrl: 'https://events.featflags.io',
})
export function App() {
const [client, setClient] = useState<FlagClient>()
useEffect(() => void flags.then(setClient), [])
if (!client) return null // or your splash screen: reading storage takes a few milliseconds
return (
// A context without a key is an anonymous user.
<FeatflagsProvider client={client} context={user ? { key: user.id, plan: user.plan } : {}} experimentSeeds={user?.experimentSeeds}>
<Home />
</FeatflagsProvider>
)
}
createClient takes the TypeScript SDK’s client options (context, experimentKey, experimentSeeds, bootstrap, hooks, onExposure, pollInterval, …), and supplies the app’s lifecycle and storage itself. You can pass your own stores:
| Option | Default | What it does |
|---|---|---|
storage |
AsyncStorage’s own featflags database, apart from your app’s data |
Where the last flags are kept: an object with AsyncStorage’s getItem, setItem and removeItem. persist: false keeps nothing. |
anonymousKeyStorage |
storage |
Where a signed-out user’s anonymous key is kept, in the same shape. |
The provider here takes a client only, not options.
Read flags
import { useFlag, useFlagDetail, useTrack } from '@featflags/react-native'
export function Checkout({ total }: { total: number }) {
const newCheckout = useFlag('new-checkout', false)
const pricing = useFlagDetail('pricing-page', 'v1') // { value, variant, reason, ruleId, prerequisite }
const track = useTrack()
return newCheckout ? <NewCheckout pricing={pricing.value} onBuy={() => track('checkout.completed', total)} /> : <OldCheckout />
}
useFlag, useFlagDetail, useTrack and useFeatflagsClient behave as in React. A change of the provider’s context or experimentSeeds value calls identify. Outside React, use the client directly: client.bool('new-checkout', false), client.identify(…), client.track(…).
In the app’s lifecycle
- Foreground and background. The client checks for changes every 60 seconds (±10%) only while the app is active, checks again as soon as it comes back (at most every 5 seconds), and sends its counts, experiment exposures and events when the app goes to the background, where the system may suspend it at any time. The iOS app switcher or a system alert changes nothing.
- Network. With NetInfo installed, it checks again as soon as the device is back online (at most every 5 seconds, in the foreground). Without it, a check that fails in the foreground is retried every 5 seconds until one gets through.
- Offline start. The last flags are stored, so a cold start without a network evaluates them;
client.cachedistrueuntil the network answers. - Anonymous users. A signed-out user without a
keygets an anonymous key, stored too, so they stay in their rollouts across launches.
The anonymous key and backups
The anonymous key should survive a cleared cache, which it does, since AsyncStorage isn’t a cache. It shouldn’t travel to another device in a restored backup, where two installs would then share one key.
- iOS. AsyncStorage 3 keeps its databases out of iCloud backups. Nothing to do.
- Android. Auto Backup includes AsyncStorage’s databases unless your app excludes them. Exclude featflags’ database in the rules your manifest points at (
android:fullBackupContentup to Android 11,android:dataExtractionRulesfrom 12):
<!-- res/xml/backup_rules.xml, android:fullBackupContent -->
<full-backup-content>
<exclude domain="database" path="async-storage/databases/featflags/" />
</full-backup-content>
<!-- res/xml/data_extraction_rules.xml, android:dataExtractionRules -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="database" path="async-storage/databases/featflags/" />
</cloud-backup>
<device-transfer>
<exclude domain="database" path="async-storage/databases/featflags/" />
</device-transfer>
</data-extraction-rules>
AsyncStorage 3 names that directory async-storage/databases/<name>/; check it with adb shell run-as <your.package> ls databases if you upgrade AsyncStorage. To keep the flags backed up and only the key out, give anonymousKeyStorage a store of its own (createAsyncStorage('featflags-device')) and exclude only that. If you pass AsyncStorage’s legacy default export instead, it’s your app-wide database, and excluding it excludes all of your app’s AsyncStorage data.
Experiments
As in a browser: keep the environment’s experiment key on your backend, and hand each signed-in user’s app their seeds (experimentSeeds(context) in a server SDK) at sign-in, passed as the provider’s experimentSeeds with the context they belong to. For anonymous use, pass experimentKey to createClient, knowing that a key shipped in an app is public. See experiments and the TypeScript SDK.
Hermes
Evaluation needs no crypto: the SDK brings its own hashing and its own engine for regex conditions. It needs TextEncoder (in Hermes from React Native 0.74) and BigInt (from 0.70). Anonymous keys use crypto.randomUUID or crypto.getRandomValues when a polyfill provides them, and Math.random otherwise: the key must be unique, not secret. Allowed origins don’t apply, because an app has no origin.
Everything else
Hooks, OpenTelemetry, lists and private segments are the TypeScript SDK’s: see its page.