---
title: "React Native SDK"
description: "React Native bindings for the NoLag core SDK: AppState lifecycle, NetInfo reachability, token caching and polyfills."
---

# @nolag/react-native

React Native bindings for [`@nolag/js-sdk`](/docs/sdks/javascript).

## Overview

This is a convenience layer, not a required one. As of `@nolag/js-sdk` 1.12.0 the core SDK works on React Native by itself, because it ships a `react-native` export condition that Metro resolves correctly. What this package adds is the platform behaviour you would otherwise have to write yourself, and probably get wrong on iOS.

| | Without this package | With it |
| --- | --- | --- |
| `TextEncoder`/`TextDecoder` | msgpack constructs both at module scope; missing globals throw on import, before your code runs | polyfilled if absent, untouched if present |
| App background/foreground | no signal at all: the core defaults to the Page Visibility API, which React Native does not have | `AppState` wired to the SDK's lifecycle adapter |
| Token expiry during suspend | JS timers do not fire reliably while suspended, so a scheduled refresh can be skipped and the held token is already dead | rechecked on every foreground |
| Network flap | reconnect backoff grows to 30s and keeps waiting after the radio returns | NetInfo collapses the backoff and retries immediately |
| Cold start | a round trip to your backend to mint a token, on the critical path | cached until shortly before `exp` |

## Installation

```bash [Terminal]
npm install @nolag/react-native @nolag/js-sdk
```

This package pulls in no native modules of its own. NetInfo and storage are injected rather than imported, because Metro resolves `require()` statically and fails the build on an unresolved module, so the usual try/catch-around-an-optional-import trick does not work as it does under Node. That is also what lets it run in Expo Go.

## Quick Start

```typescript [TypeScript]
import { NoLag } from '@nolag/react-native'

// Pass a TokenProvider, never a token string.
const client = NoLag(async () => {
  const res = await fetch('https://your-api.example/nolag-token')
  return (await res.json()).token
})

await client.connect()
```

It returns a genuine `NoLagSocket`, so it drops straight into the app SDKs:

```typescript [TypeScript]
import { NoLag } from '@nolag/react-native'
import { NoLagChat } from '@nolag/chat'

const client = NoLag(tokenProvider)
const chat = new NoLagChat({ client, username: 'Alice' })

await client.connect()
await chat.ready()
```

An actor access token embedded in an app bundle is extractable from the IPA or APK by anyone who downloads your app. The provider should call your backend, which mints a short-lived [client token](/docs/client-tokens) using a project signing key that never leaves your server.

## Network reachability

Pass the NetInfo module in and the client reconnects the moment the radio returns, instead of waiting out a backoff that has already grown to 30 seconds.

```typescript [TypeScript]
import NetInfo from '@react-native-community/netinfo'

const client = NoLag(tokenProvider, { netInfo: NetInfo })
```

Omit it and the client still reconnects, just on the normal backoff schedule.

## Token caching

Skip the token round trip on warm launches. Storage is injected for the same reason as NetInfo.

```typescript [MMKV]
import { MMKV } from 'react-native-mmkv'
import { NoLag, createCachedTokenProvider, fromMMKV } from '@nolag/react-native'

const storage = new MMKV()

const client = NoLag(
  createCachedTokenProvider({
    provider: fetchTokenFromYourBackend,
    storage: fromMMKV(storage)
  })
)
```

```typescript [AsyncStorage]
import AsyncStorage from '@react-native-async-storage/async-storage'

// AsyncStorage satisfies the TokenStore contract directly, no adapter needed.
createCachedTokenProvider({ provider, storage: AsyncStorage })
```

Opaque (non-JWT) tokens are never cached: without an `exp` claim there is no safe way to know when to stop using one. Storage failures are non-fatal, so a locked keychain or a full disk degrades to an uncached fetch rather than a failed connection.

## Backgrounding

```typescript [TypeScript]
const client = NoLag(tokenProvider, { disconnectOnHidden: true })
```

Off by default. iOS reports `inactive` for transient states, meaning the app switcher, an incoming call, Control Centre and the biometric prompt. Those are ignored, so only a real background drops the socket.

## Cleaning up

`disconnect()` deliberately keeps the AppState and NetInfo subscriptions alive, because a backgrounded client has to be able to come back. When the client itself is going away, call `destroy()`:

```typescript [TypeScript]
useEffect(() => {
  const client = NoLag(tokenProvider)
  client.connect()
  return () => client.destroy()
}, [])
```

## Options

Everything `NoLagOptions` accepts, plus:

| Option | Type | Default |
| --- | --- | --- |
| `netInfo` | `NetInfoModule` | none (no reachability signal) |
| `lifecycle` | `LifecycleAdapter \| null` | AppState adapter; `null` disables |
| `network` | `NetworkAdapter \| null` | from `netInfo` if given; `null` disables |

`lifecycle` and `network` take precedence over `netInfo` when set explicitly.

## Advanced: bringing your own adapters

`NoLag()` builds both adapters for you, so most apps never need this. Reach for it when your app-state signal does not come from `AppState`, when you use something other than NetInfo, or when you want the same decisions the SDK makes available to your own reconnect logic.

| Export | Purpose |
| --- | --- |
| `createAppStateLifecycleAdapter(appState?)` | Lifecycle adapter from anything with `addEventListener` |
| `mapAppState(status)` | The status mapping, or `null` for states that should not move the client |
| `createNetInfoNetworkAdapter(netInfo)` | Network adapter, built explicitly |
| `isReachable(state)` | Whether a NetInfo state means "worth retrying now" |
| `decodeJwtExp(token)` | Read the `exp` claim without verifying, or `null` |
| `fromMMKV(mmkv)` | Adapt a `react-native-mmkv` instance to `TokenStore` |

Two of those encode a judgement worth knowing. `mapAppState` returns `null` for `inactive` because treating it as background would tear the socket down every time someone swipes down on Control Centre. And `isReachable` treats connected-but-unknown as reachable, because `isInternetReachable` is `null` while NetInfo is still probing and that is exactly the transition worth acting on: a wasted retry costs one connection attempt, a missed signal costs up to 30 seconds.

## WebRTC is not supported yet

`WebRTCManager` is not exported here, and the core SDK's React Native build omits it too. Its Node path does a bare require of the `wrtc` package, which Metro collects statically. `allowOptionalDependencies` is on under `@expo/metro-config` but off under bare `@react-native/metro-config`, so shipping it would bundle fine on Expo and fail the build on bare React Native.

Voice and video on React Native need `react-native-webrtc`, which is separate work. Everything else in the SDK is available.

## Requirements

- React Native >= 0.71
- `@nolag/js-sdk` >= 1.12.0 (peer dependency)

`@nolag/js-sdk` must resolve to a single copy in your tree. `NoLagSocket` has private fields, so TypeScript compares it close to nominally: two copies make passing a client into an app SDK fail to typecheck with a thoroughly unhelpful error. If that happens, add a `resolutions` (Yarn) or `overrides` (npm) entry pinning a single version.
