# React Native and Expo

`@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](/docs/apps) first for what differs from the web.

## Install

The quickest way, in the app folder:

```bash
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:

```bash
npx expo install @splithub/react-native @react-native-async-storage/async-storage expo-application expo-device
```

Bare React Native (0.72 or newer):

```bash
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

```tsx
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):

```tsx
import config from './splithub.config';

<SplitHubProvider config={config} property="sh-x1h78w">
```

Without Expo, pass the app info yourself:

```tsx
<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](/docs/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`:

```tsx
import { SplitHubProvider, ExpoRouterScreens } from '@splithub/react-native';

export default function RootLayout() {
  return (
    <SplitHubProvider config={config}>
      <ExpoRouterScreens />   {/* `/`, `/product/42`, ... */}
      <Stack />
    </SplitHubProvider>
  );
}
```

React Navigation:

```tsx
<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:

```ts
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:

```bash
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.
