Introducing the Zenmanage React Native SDK
The Zenmanage React Native SDK brings FlagsProvider, useFlag, and useVariant to Expo and bare React Native apps, with on-device caching and background refresh built in.
@zenmanage/react-native. It works in Expo and bare React Native apps, runs one codebase on iOS and Android, and can keep the last rules it received on the device.
Changing a feature in a mobile app usually means a new build. Once a version is in the App Store or Google Play, turning something on for one segment of users, or turning it off because it's misbehaving, waits on a release, a store review, and users who update when they update. Feature flags move that decision out of the binary. The flag ships in the app, and the value comes from Zenmanage.
The React Native SDK is the mobile version of our React SDK. It wraps the same core runtime as @zenmanage/sdk, so targeting, defaults, and evaluation behave the same way they do everywhere else in your stack. What it adds is what a mobile app needs: a provider that follows the app's lifecycle, a persistent cache, and hooks that fit React Native rendering.
One codebase for iOS and Android
A flag doesn't belong to a platform. A new checkout flow, a new onboarding step, or a kill switch for a payment provider is one flag key, and it applies to the iOS and Android builds that come out of the same code. Targeting rules, percentage rollouts, and variants live in Zenmanage, so changing the split for either platform doesn't mean shipping a new build to either store.
Getting started
Install the package along with the core SDK. The command is the same in Expo and bare React Native projects:
npm install @zenmanage/react-native @zenmanage/sdk react react-native
Then wrap your app in FlagsProvider with a mobile key, and read a flag anywhere below it:
import React from 'react';
import { Text, View } from 'react-native';
import { FlagsProvider, useFlag } from '@zenmanage/react-native';
function CheckoutLabel() {
const { value: enabled, isLoading } = useFlag('new-checkout', false);
if (isLoading) {
return <Text>Loading flags...</Text>;
}
return <Text>{enabled ? 'Checkout (New)' : 'Checkout'}</Text>;
}
export default function App() {
return (
<FlagsProvider environmentToken="mob_your_mobile_key_here">
<View style={ { padding: 24 } }>
<CheckoutLabel />
</View>
</FlagsProvider>
);
}
The code doesn't change between Expo and bare React Native. The difference is where the key comes from. An Expo app can read it from an EXPO_PUBLIC_ environment variable, and a bare app can read it from whatever build-time configuration it already uses. The key goes in the app, so use a mobile key, not a server key.
Hooks and components
useFlag takes a default and returns it until the flag resolves, so a screen always has a value to render. The type of the default decides how the flag is read: a boolean, a string, a number, or an object or array for a JSON flag.
const { value: enabled } = useFlag('new-checkout', false);
const { value: steps } = useFlag<string[]>('onboarding-steps', []);
For experiments, useVariant reads a string flag as a variant name and falls back to 'control' unless you pass something else. Which users see which variant is decided in Zenmanage, not in the app.
Most apps also have a login. useIdentify tells the SDK who the current user is, so targeting rules apply to them and not to an anonymous device:
import { Context } from '@zenmanage/sdk';
import { useIdentify } from '@zenmanage/react-native';
const { identify, clearIdentity } = useIdentify();
await identify(Context.single('user', 'user-123')); // after login
await clearIdentity(); // after logout
Both calls refresh the rules by default. identify evaluates against the new user, and clearIdentity drops the user context and evaluates again.
If you'd rather not branch inside a component, FlagGate renders its children only when the flag is on, with fallbacks for the disabled and loading states:
import { FlagGate } from '@zenmanage/react-native';
<FlagGate flagKey="beta-chat" disabledFallback={<LegacyChat />}>
<BetaChat />
</FlagGate>
Local caching
Phones lose signal, and apps get killed in the background. AsyncStorageCache keeps the last rules the SDK received on the device, across app restarts. Without a persistent cache, the SDK keeps rules in memory only, and a cold start has nothing to work from until the first fetch completes.
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AsyncStorageCache, FlagsProvider } from '@zenmanage/react-native';
const cache = new AsyncStorageCache({
storage: AsyncStorage,
keyPrefix: 'zenmanage-mobile',
});
<FlagsProvider environmentToken="mob_your_mobile_key_here" cache={cache} cacheTtl={300}>
<App />
</FlagsProvider>
cacheTtl sets how long cached rules stay valid, in seconds. Install @react-native-async-storage/async-storage to use it. It's an optional dependency, and we recommend it for any app that ships to real users.
Background refresh that matches how apps run
Users move an app between foreground and background all day. A flag that changed while the app was in the background should be current when someone comes back to it. The provider handles that with three settings:
refreshOnForegroundrefreshes the rules when the app returns to the foreground. It's on by default.minForegroundRefreshIntervalMssets the minimum gap between foreground refreshes, so rapid app switching doesn't turn into rapid requests. The default is 30 seconds.backgroundRefreshIntervalMsrefreshes the rules on a fixed interval while the app is running. It's off unless you set it.
Background and foreground refreshes never show your loading fallback. Gated UI stays mounted while the new rules arrive, and its value updates when they land. A refresh shouldn't flicker a screen the user is looking at.
Two things to know before you ship
Use the right key for the app
Mobile apps use a mobile key, prefixed mob_. Client keys, prefixed cli_, also work for browser-compatible flows. Server keys, prefixed srv_, are rejected by this package. Anything bundled into an app can be extracted, and a server key in an app is a leaked secret.
Pick defaults you'd be happy to ship
If the API can't be reached, hooks resolve to the default you passed. That's the safety net, so choose the default that keeps the current experience. A new feature should default to off, and a kill switch should default to the safe path, the one you want if the flag can't be read at all.
What's next
@zenmanage/react-native is on npm, and the source is on GitHub, with runnable Expo and bare React Native examples and the full changelog. The React Native quickstart walks through setup from start to finish, and the SDK overview lists every language we support.
If something in the package gets in your way, tell us at hello@zenmanage.com or open an issue on GitHub.