---
url: https://simpledatalogs.com/docs/quickstarts/react-native.md
description: >-
  Send events, sessions and crashes from a React Native or Expo app to
  simpledatalogs with the JavaScript SDK.
---

# React Native / Expo

The JavaScript SDK detects React Native and runs in app mode: no browser autocapture, sessions driven by `AppState`, and the platform's own User-Agent.

## Install

```sh
npx expo install simpledatalogs @react-native-async-storage/async-storage
npx simpledatalogs init   # adds EXPO_PUBLIC_SIMPLEDATALOGS_KEY and simpledatalogs.ts
```

## Add it

```ts
// simpledatalogs.ts
import AsyncStorage from '@react-native-async-storage/async-storage'
import { AppState } from 'react-native'
import { init } from 'simpledatalogs'

export const sdl = init({
  key: process.env.EXPO_PUBLIC_SIMPLEDATALOGS_KEY,
  storage: AsyncStorage, // unsent events survive the app being killed
  appState: AppState, // flush on background, sessions
  autocapture: true,
  defaultProperties: { app_version: '1.4.0' },
})

// Crashes and uncaught errors, as unhandled exceptions.
sdl.installGlobalHandlers()
```

```ts
import { sdl } from './simpledatalogs'

sdl.setDeviceId(installId) // an anonymous id you keep, for active devices
sdl.track('level_complete', { level: 3 })
sdl.track('screen_view', { screen: 'Settings' })
```

## What each option does

| Option | What you get |
|---|---|
| `storage: AsyncStorage` | The outbox is saved at most once a second, and at once when the app goes to the background. Events left from the last launch go out after the next one starts. Without it, the outbox is in memory and lost if the app is killed |
| `appState: AppState` | Going to the `background` saves and flushes. Without it, call `sdl.handleAppStateChange(state)` from your own listener |
| `autocapture: true` | A session from launch: `session_start`, a `session_id` on every event, and a new session when the app returns after 30 minutes or more in the background. `session_end` is stamped when the app left the foreground. A launch in the background (a background task) starts no session until the app is active |
| `installGlobalHandlers()` | Chains `ErrorUtils`: each uncaught error is sent with its Hermes stack frames, and a fatal one waits up to 1 second for the flush before the app's previous handler runs |

Any storage with `getItem`, `setItem` and `removeItem` works, whether they return Promises or values.

## One key per platform

Create an iOS deployment and an Android deployment in the same environment, and pick the public key at runtime, so the dashboard splits the platforms:

```ts
import { Platform } from 'react-native'

const key = Platform.OS === 'ios' ? process.env.EXPO_PUBLIC_SIMPLEDATALOGS_KEY_IOS : process.env.EXPO_PUBLIC_SIMPLEDATALOGS_KEY_ANDROID
export const sdl = init({ key, storage: AsyncStorage, appState: AppState, autocapture: true })
```

## Errors you catch

```ts
try {
  await purchase()
} catch (error) {
  sdl.captureException(error, { props: { sku: 'pro_monthly' } })
}
```

::: tip Native crashes
The SDK sees JavaScript errors only. Crashes in Swift, Kotlin or C++ code, and out-of-memory kills, never reach the JS thread, so a JS-only sender can't report them.
:::

## Re-sending safely

An event you queue yourself (an offline purchase log, for example) can keep its own id, so sending it twice counts once:

```ts
sdl.track('purchase', { sku }, { eventId: purchase.uuid, ts: purchase.createdAt })
```

## In the first minute

* Your first events on the dashboard within a second or two, or at once with `await sdl.flush()`.
* With `setDeviceId`, the device counts in **active devices**.
* Track screens with `screen_view` events, for example from your navigation container's `onStateChange`.

## Against a local ingest

* **Android emulator:** the Mac's `localhost` is `10.0.2.2`, so use `endpoint: 'http://10.0.2.2:4001'`.
* **iOS simulator:** `http://localhost:4001` works as is.
* **Phone over USB:** run `adb reverse tcp:4001 tcp:4001` and keep `localhost`, or use the Mac's LAN address.
