React Native and Expo
@splithub/react-native: install with npx splithub init, SplitHubProvider, Expo Router and React Navigation screens, hooks, offline queue, release builds.
@splithub/react-native runs experiments and collects analytics in React Native and Expo apps. Variants are decided on the device before the first render, screens land in the same reports as your website, and events wait on the device while it is offline. Read Apps first for what differs from the web.
Install
The quickest way, in the app folder:
npx splithub init --mode cloud --yes
init detects Expo or React Native, installs the package and its storage module (through npx expo install on Expo), and wraps your entry in <SplitHubProvider>: app/_layout.tsx with Expo Router, else App.tsx. It also adds screen tracking, the agent skills and the MCP entry. When it cannot find the entry, it prints the snippet for you.
By hand, Expo:
npx expo install @splithub/react-native @react-native-async-storage/async-storage expo-application expo-device
Bare React Native (0.72 or newer):
npm i @splithub/react-native @react-native-async-storage/async-storage
cd ios && pod install
Storage keeps the visitor, their variants and unsent events between launches. The package uses react-native-mmkv when it is installed, else AsyncStorage. expo-application and expo-device report the app version and device model; without Expo, pass them yourself (below).
Provider
import { SplitHubProvider, Experiment, Variant, useTrack } from '@splithub/react-native';
export default function App() {
return (
<SplitHubProvider publicKey="pk_..." property="sh-x1h78w">
<Home />
</SplitHubProvider>
);
}
function Home() {
const track = useTrack();
return (
<>
<Experiment id="welcome">
<Variant name="control"><Text>Welcome to the shop.</Text></Variant>
<Variant name="short"><Text>Shop in two taps.</Text></Variant>
</Experiment>
<Button title="Get started" onPress={() => track('Get Started')} />
</>
);
}
publicKey is your project's public key (pk_...), property the id of the app's analytics property. Create the welcome experiment in the dashboard; until it runs, everyone sees the control.
If you keep experiments in splithub.config.ts, pass the config instead (this is what init writes):
import config from './splithub.config';
<SplitHubProvider config={config} property="sh-x1h78w">
Without Expo, pass the app info yourself:
<SplitHubProvider publicKey="pk_..." app={{ appId: 'com.acme.shop', version: '2.4.1', build: '412' }}>
app also takes device (mobile | tablet | desktop), storage (an MMKV instance, AsyncStorage, or any synchronous getItem / setItem / removeItem store) and extra targeting context.
Hooks
The same as in React, imported from @splithub/react-native:
| Hook | Returns |
|---|---|
useVariant(id) |
the variant key; records an exposure the first time |
useValue(id) |
the payload of a value experiment |
useTrack() |
(name, props?) => void: an event, any name |
useIdentify() |
(userId, traits?) => void; null on sign-out |
useScreen(name) |
reports a screen yourself |
useAssignment(id), useSplitHub() |
the full decision, the core client |
Screens
Expo Router, once in app/_layout.tsx:
import { SplitHubProvider, ExpoRouterScreens } from '@splithub/react-native';
export default function RootLayout() {
return (
<SplitHubProvider config={config}>
<ExpoRouterScreens /> {/* `/`, `/product/42`, ... */}
<Stack />
</SplitHubProvider>
);
}
React Navigation:
<SplitHubProvider config={config}>
<NavigationContainer>
<NavigationScreens /> {/* `/Home`, `/Settings`, ... */}
<Tabs />
</NavigationContainer>
</SplitHubProvider>
<NavigationScreens /> finds the container; pass navigationRef={ref} if you hold one. The hooks useExpoRouterTracking() and useNavigationTracking(ref) do the same by hand. Anything else: useScreen('Checkout').
Launch
AsyncStorage is read into memory once at launch, so variants stay synchronous. The provider renders fallback (nothing by default) during that read. To start it earlier, call prepare() before the splash screen hides:
import { prepare } from '@splithub/react-native';
import * as SplashScreen from 'expo-splash-screen';
SplashScreen.preventAutoHideAsync();
prepare().finally(() => SplashScreen.hideAsync());
With MMKV there is nothing to wait for. The very first launch also waits up to 400 ms for the dashboard's experiments (control meanwhile); later launches use the saved copy and refresh it in the background.
Debug builds send nothing
A development build (every Metro debug bundle) sends nothing to SplitHub Cloud, so your own taps never count. Variants still render. To see events arrive, run a release build:
npx expo start --no-dev --minify
npx expo run:ios --configuration Release
npx expo run:android --variant release
Then check with npx splithub analytics events <property>, and run npx splithub doctor to check the package, the storage module and the provider.
Example
examples/expo-app in the SDK repository: Expo Router, an experiment, screens and an event.