React Native SDK
The Experimentation Platform React Native SDK provides feature flag evaluation, A/B experiment assignment, and event tracking for React Native applications. It includes React hooks, a context provider, and AsyncStorage-backed offline fallback.
Features
- Consistent MD5 hashing for cross-SDK compatible bucket assignment
- React hooks:
useFlag,useExperiment,useExperimentationClient ExperimentationProvidercontext for tree-wide access- In-memory cache with configurable TTL
- AsyncStorage offline persistence
- TypeScript-first with full type definitions
- Zero React Native-specific native modules (fetch + AsyncStorage only)
Installation
npm install @experimentation-platform/react-native-sdk md5 @react-native-async-storage/async-storage
# or
yarn add @experimentation-platform/react-native-sdk md5 @react-native-async-storage/async-storage
iOS additional step
cd ios && pod install
This is required for @react-native-async-storage/async-storage.
Provider Setup
Wrap your root component with ExperimentationProvider:
import {
ExperimentationProvider,
ExperimentationClient,
} from '@experimentation-platform/react-native-sdk';
const client = new ExperimentationClient({
apiKey: 'your-api-key',
baseUrl: 'https://api.getexperimently.com',
cacheTtlMs: 300_000, // 5 minutes (default)
timeoutMs: 5_000, // 5 seconds (default)
offlineFallback: true, // true (default)
});
export default function App() {
return (
<ExperimentationProvider
client={client}
userId="user-123"
attributes={{ plan: 'pro', country: 'US' }}
>
<YourAppNavigator />
</ExperimentationProvider>
);
}
useFlag Hook
import { useFlag } from '@experimentation-platform/react-native-sdk';
function DarkModeToggle() {
const { value, loading, error } = useFlag('dark-mode');
if (loading) return <ActivityIndicator />;
if (error) return <Text>Error: {error.message}</Text>;
return (
<Switch
value={value}
onValueChange={() => {/* your logic */}}
/>
);
}
The hook re-runs when userId or flagKey changes.
useExperiment Hook
import { useExperiment } from '@experimentation-platform/react-native-sdk';
function CheckoutPage() {
const { variant, loading, error } = useExperiment('checkout-experiment');
if (loading) return <ActivityIndicator />;
if (error) return <OldCheckout />;
return variant === 'treatment' ? <NewCheckout /> : <OldCheckout />;
}
useExperimentationClient Hook
For imperative access to the client (e.g. manual event tracking):
import { useExperimentationClient } from '@experimentation-platform/react-native-sdk';
function PurchaseButton() {
const { client, userId } = useExperimentationClient();
const handlePress = async () => {
// ... handle purchase ...
await client.track('purchase_completed', userId, {
amount: 49.99,
currency: 'USD',
});
};
return <Button title="Buy Now" onPress={handlePress} />;
}
Direct Client API
You can also use the client directly, without hooks:
import { ExperimentationClient } from '@experimentation-platform/react-native-sdk';
const client = new ExperimentationClient({
apiKey: 'your-api-key',
baseUrl: 'https://api.getexperimently.com',
});
// Feature flag evaluation
const enabled = await client.evaluateFlag('dark-mode', 'user-123', {
plan: 'pro',
country: 'US',
});
// Experiment assignment
const variant = await client.getAssignment('checkout-experiment', 'user-123');
// Event tracking (fire-and-forget, never throws)
await client.track('button_clicked', 'user-123', { screen: 'home' });
// Clear in-memory cache
client.clearCache();
Evaluation Order
For evaluateFlag and getAssignment:
- In-memory cache — fast path, returns cached result if not expired.
- API call — fetches and evaluates locally using the consistent hash.
- AsyncStorage fallback — serves last-known value when the API is unreachable.
- Safe default — returns
false/nullif no data is available.
AsyncStorage Configuration
The SDK uses @react-native-async-storage/async-storage with keys prefixed
ep_sdk_flag: and ep_sdk_asgn: to avoid collisions.
To disable offline persistence:
const client = new ExperimentationClient({
apiKey: 'your-api-key',
baseUrl: 'https://api.getexperimently.com',
offlineFallback: false,
});
Platform-Specific Notes
iOS
- Requires
pod installfor the AsyncStorage native module. - Tested on iOS 14+. Works with both old and new architecture (Fabric).
Android
- No additional setup beyond standard React Native configuration.
- Tested on Android API 21+ (minSdk 21).
- AsyncStorage uses SQLite on Android by default.
Expo
If using Expo Managed Workflow:
npx expo install @react-native-async-storage/async-storage
The SDK works with Expo SDK 49+.
Hash Algorithm
The consistent hash is identical across all SDK implementations:
MD5("{userId}:{flagKey}") → first 4 bytes as little-endian uint32 → ÷ 2^32
The divisor is 4294967296 (2^32), not 4294967295 (MaxUInt32). This
ensures bit-identical results with the Java, Python, Go, iOS, Android, and
Flutter SDKs.
import { hashUser } from '@experimentation-platform/react-native-sdk';
const h = hashUser('user-123', 'my-flag');
// h ≈ 0.6927449859 (verified against all SDK implementations)
Testing
Jest Setup
The SDK ships with a Jest mock for AsyncStorage. Add it to your Jest config:
{
"moduleNameMapper": {
"@react-native-async-storage/async-storage": "<rootDir>/node_modules/@experimentation-platform/react-native-sdk/__mocks__/@react-native-async-storage/async-storage"
}
}
Or copy the mock to your project's __mocks__ directory.
Mocking the Client in Tests
import { ExperimentationClient } from '@experimentation-platform/react-native-sdk';
const mockClient = {
evaluateFlag: jest.fn().mockResolvedValue(true),
getAssignment: jest.fn().mockResolvedValue('treatment'),
track: jest.fn().mockResolvedValue(undefined),
clearCache: jest.fn(),
} as unknown as ExperimentationClient;
Testing Hooks with ExperimentationProvider
import { render, waitFor } from '@testing-library/react-native';
import { ExperimentationProvider, useFlag } from '@experimentation-platform/react-native-sdk';
function TestComponent() {
const { value, loading } = useFlag('my-flag');
if (loading) return <Text>Loading</Text>;
return <Text testID="result">{value ? 'on' : 'off'}</Text>;
}
test('useFlag returns true', async () => {
const client = { evaluateFlag: jest.fn().mockResolvedValue(true) } as any;
const { getByTestId } = render(
<ExperimentationProvider client={client} userId="u1">
<TestComponent />
</ExperimentationProvider>
);
await waitFor(() => {
expect(getByTestId('result').props.children).toBe('on');
});
});
Troubleshooting
useExperimentationContext must be used inside an <ExperimentationProvider>
Ensure ExperimentationProvider wraps the component calling the hook.
Check that there is no second copy of React in your bundle (version mismatch).
Flags always return false
- Verify your
apiKeyandbaseUrlare correct. - Check network connectivity — the SDK returns
falseas a safe default on errors. - Enable
offlineFallback: trueto serve cached values during outages. - Check the flag's rollout percentage in the platform dashboard.
Type errors with md5
Install the type definitions:
npm install --save-dev @types/md5