> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trevosdk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# React Native

> Experiments and tracking for React Native and Expo, bucketing users identically to the web.

`@trevosdk/react-native` runs experiments and records events in React Native and Expo apps,
assigning the same variants the web SDK does for the same user.

Pure TypeScript — no native module, no linking step, no config plugin.

Expo:

```bash theme={null}
npm install @trevosdk/react-native
npx expo install @react-native-async-storage/async-storage
```

Bare React Native:

```bash theme={null}
npm install @trevosdk/react-native @react-native-async-storage/async-storage
```

AsyncStorage is a **required** peer, not an optional one. Without persistence the anonymous
id is re-minted on every launch, which re-buckets every user and corrupts any running
experiment.

## Quick start

Create the client in its own module. This example uses Expo's public environment
convention; in bare React Native, use the app's existing runtime-config adapter.

```ts theme={null}
// lib/trevo.ts
import { createClient } from '@trevosdk/react-native';

const apiKey = process.env.EXPO_PUBLIC_TREVO_API_KEY;
if (!apiKey) throw new Error('Missing EXPO_PUBLIC_TREVO_API_KEY');

export const trevo = createClient({ apiKey });
```

Keep the app's existing splash/loading boundary visible while the hook returns
`null`, so AsyncStorage hydration cannot flash control to a treatment user. If
auth restores a known user, call `trevo.identify(user.id)` before mounting this
experiment UI.

```tsx theme={null}
// hooks/useCheckoutVariant.ts
import { useEffect, useState } from 'react';
import { trevo } from '../lib/trevo';

export function useCheckoutVariant(): string | null {
  const [variant, setVariant] = useState<string | null>(null);

  useEffect(() => {
    let mounted = true;
    let ready = false;
    const apply = () => {
      if (mounted && ready) setVariant(trevo.getVariant('checkout-v2'));
    };
    const unsubscribe = trevo.onChange(apply);
    void trevo.ready().then(() => {
      ready = true;
      apply();
    });
    return () => {
      mounted = false;
      unsubscribe();
    };
  }, []);

  return variant;
}

export function trackAddToCart(): void {
  trevo.track('add_to_cart', { sku: 'ABC' });
}
```

Use a **publishable** key (`tsk_live_…`). A shipped binary can be unpacked, so a secret key
in an app is a leaked key.

## Read this before you plan your first mobile experiment

Three properties of mobile are not choices we made, and every vendor shares them. Planning
around them is easier than discovering them mid-experiment.

**A mobile cohort ramps over weeks, not days.** Experiment code ships inside an app release,
so your population is whoever has updated. Web reaches full traffic within a day of
activation; mobile climbs as adoption climbs, and both variants coexist across app versions
for as long as old builds stay installed.

**An experiment can be switched off without a release, but not changed.** Config is re-read
on every foreground, so pausing or stopping is immediate. Changing what a variant *does* is
code, and code ships through App Store review.

**Anonymous users are per-device.** A visitor who browses on a laptop and then opens the app
is two participants until they sign in. `identify()` links them from that point forward;
nothing can link them retroactively. Identifying a user can change their arm because all SDKs
assign the signed-in user from the same user id. Restore auth and call `identify()` before
showing experiment UI; the `onChange` subscription above re-renders if identity changes later.

## Durability

Phones lose the network constantly, are killed without warning, and have wrong clocks more
often than desktops. The SDK is built around that:

* **Events survive a cold start.** The queue is written to AsyncStorage and replayed on next
  launch. Delivery is at-least-once, and every event carries an idempotency key so a replay
  is recorded once.
* **Timestamps survive a wrong clock.** Each batch is stamped with `sentAt` when it leaves
  the device. Ingestion compares that against its own clock to recover the device's offset
  and correct every event in the batch — which is what lets a queue drained three days late
  land on a real timeline. Nothing is dropped for being out of range.
* **Assignment survives no network at all.** Config is persisted, so a cold start in a lift
  assigns the same variant it assigned yesterday instead of falling back to control and
  silently switching once the network returns.
* **A signed-in identity survives a relaunch.** `identify()` persists the user id, so the next
  cold start buckets on it rather than reverting to the anonymous id and splitting one person
  across two identities.

## Gating on app version

An experiment whose variant code only exists from a given build onward should not enrol
devices that cannot render it. Set `minAppVersion` on the experiment:

```json theme={null}
{ "experimentKey": "checkout-v2", "minAppVersion": { "ios": "1.4.0", "android": "1.4.0" } }
```

Builds below the threshold get `control` and record no exposure, so they never appear in the
results. This requires you to pass `appVersion` when creating the client — without it, a
gated experiment returns control for everyone, because the SDK cannot prove the build is new
enough and guessing wrong means a broken screen.

Always pass the installed binary's real app version, never a copied sample. Expo apps can read
it from `expo-application` (`npx expo install expo-application`):

```ts theme={null}
import * as Application from 'expo-application';

const appVersion = Application.nativeApplicationVersion;
if (!appVersion) throw new Error('Missing Expo app version');
export const trevo = createClient({ apiKey, appVersion });
```

For bare React Native, pass the value from the build-metadata adapter the app
already uses. Omit it when the workspace does not use `minAppVersion`; do not
hard-code it.

## Options

| Option | Default | Notes |
| - | - | - |
| `apiKey` | — | Required. `tsk_live_…` |
| `appVersion` | — | This build's version. Required for `minAppVersion` gating |
| `platform` | `Platform.OS` | Override for tests |
| `bootstrapConfig` | — | Assign before storage and network answer |
| `pollIntervalMs` | `300000` | Foreground config refresh happens regardless |
| `flushIntervalMs` | `15000` | Also flushes on background |
| `maxBatchSize` | `50` | Capped at 500 by the server |
| `storage` | AsyncStorage | Override for tests |
| `appState` | RN AppState | Override for tests |
| `configUrl` | — | Endpoint override for proxied installs |
| `ingestionUrl` | — | Endpoint override for proxied installs |
| `fetch` | global | Override for tests |
| `onError` | `console.warn` | Background failures surface here |

## API summary

| Method | Purpose |
| - | - |
| `getVariant(key, { trackExposure }?)` | Deterministic and synchronous. Returns `control` until `ready()` resolves |
| `track(event, properties?, { insertId }?)` | Queue an event |
| `identify(userId)` | Associate subsequent events with a user and re-resolve assignment from that user id |
| `reset()` | Clear the user and mint a new anonymous id. Call on sign-out |
| `getAnonymousId()` | The device's stable id, once storage has been read |
| `ready()` | Resolves once stored state is loaded and the first fetch settles |
| `onChange(listener)` | Subscribe to config and identity changes; returns an unsubscribe |
| `flush()` / `shutdown()` | Send queued events now / stop timers and flush once |

## Expo

Works in Expo Go and in development builds with no config plugin — there is no native code
to link. Install AsyncStorage the Expo way:

```bash theme={null}
npx expo install @react-native-async-storage/async-storage
```

## Store compliance

**Apple.** The package ships `ios/PrivacyInfo.xcprivacy`. Xcode aggregates third-party
manifests automatically — an app embedding an SDK without one is rejected at submission. It
declares product interaction and a user id, both linked, neither used for tracking, and no
required-reason APIs.

**Google Play.** Data safety answers you can paste into the Play Console:

| Question | Answer |
| - | - |
| Does your app collect or share user data? | Yes |
| Data type | App activity → App interactions |
| Data type | App info and performance → Crash logs (only if you forward SDK errors) |
| Data type | Device or other IDs → **No** — the id is generated by the SDK, not read from the device |
| Collected or shared? | Collected |
| Is it processed ephemerally? | No |
| Is collection required? | Optional, if you gate initialisation on consent |
| Purpose | Analytics; App functionality |
| Is data encrypted in transit? | Yes |
| Can users request deletion? | Yes — via your Trevo workspace |

The anonymous id is minted on device and is not an advertising or hardware identifier, which
is why "Device or other IDs" is **No**. Declaring it there invites a policy review you do
not need.

## Correctness

This package runs the shared conformance vectors against its public API in CI, so a change
that would bucket a user differently from the browser fails the build. The normative rules
are in the [bucketing spec](/reference/bucketing-spec).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.