@nolag/react-native

React Native bindings for @nolag/js-sdk.

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 packageWith it
TextEncoder/TextDecodermsgpack constructs both at module scope; missing globals throw on import, before your code runspolyfilled if absent, untouched if present
App background/foregroundno signal at all: the core defaults to the Page Visibility API, which React Native does not haveAppState wired to the SDK's lifecycle adapter
Token expiry during suspendJS timers do not fire reliably while suspended, so a scheduled refresh can be skipped and the held token is already deadrechecked on every foreground
Network flapreconnect backoff grows to 30s and keeps waiting after the radio returnsNetInfo collapses the backoff and retries immediately
Cold starta round trip to your backend to mint a token, on the critical pathcached until shortly before exp

Installation

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

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:

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

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.

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)
  })
)

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

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():

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

Options

Everything NoLagOptions accepts, plus:

OptionTypeDefault
netInfoNetInfoModulenone (no reachability signal)
lifecycleLifecycleAdapter | nullAppState adapter; null disables
networkNetworkAdapter | nullfrom 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.

ExportPurpose
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.