Docs / Apps View as Markdown

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.

SplitHub
See what works. Ship what wins.