@nolag/react-native
React Native bindings for @nolag/js-sdk. MIT licensed.
Overview
This is a convenience layer, not a required one. Since @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. What this package adds is the platform behaviour you would otherwise have to write yourself:
| 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: 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 token refresh can be skipped | the token is rechecked on every foreground |
| Network flap | reconnect backoff grows to 30 s 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 | the token is cached until shortly before exp |
Everything else, including topics, rooms, presence, filters and the reconnect rules, is the JavaScript SDK unchanged; read the JavaScript SDK page for those.
Installation
npm install @nolag/react-native @nolag/js-sdkNo native modules. This package pulls in no native modules of its own. NetInfo and storage are passed in rather than imported, because Metro resolves require() statically and fails the build on an unresolved module. That is also what lets it run in Expo Go.
Quick start
import { NoLag } from "@nolag/react-native";
// Pass a TokenProvider that asks your backend for a short-lived client token
const client = NoLag(
async () => {
const res = await fetch("https://your-api.example/nolag-token");
return (await res.json()).token;
},
{ url: "wss://realtime.example.com/ws" },
);
const room = client.setApp("demo").setRoom("general");
room.on("messages", (data) => console.log(data));
// Subscribe on every connection, including after reconnects and foregrounding
client.on("connect", () => {
room.subscribe("messages");
});
await client.connect();Always pass url: the core SDK's built-in default points at the retired NoLag Cloud broker. On a device or emulator, localhost is the device itself, so point it at your kraken's reachable address (for example ws://10.0.2.2:8080/ws from the Android emulator to the host machine), and use wss:// behind TLS in production.
NoLag() returns a genuine NoLagSocket, so it drops straight into the Blueprint SDKs:
import { NoLag } from "@nolag/react-native";
import { NoLagChat } from "@nolag/chat";
const client = NoLag(tokenProvider, { url: "wss://realtime.example.com/ws" });
const chat = new NoLagChat({ client, appName: "chat", username: "Alice" });
await client.connect();
await chat.ready();Never ship a long-lived token. An actor access token embedded in an app bundle can be extracted from the IPA or APK by anyone who downloads your app. The provider should call your backend, which mints a short-lived client token with a 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 may have grown to 30 seconds:
import NetInfo from "@react-native-community/netinfo";
import { NoLag } from "@nolag/react-native";
const client = NoLag(tokenProvider, {
url: "wss://realtime.example.com/ws",
netInfo: NetInfo,
});Omit it and the client still reconnects, on the normal backoff schedule.
Token caching
Skip the token round trip on warm launches. Storage is passed in 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),
}),
{ url: "wss://realtime.example.com/ws" },
);A cached token is treated as spent 60 seconds before its exp (skewSeconds), and stored under nolag.client-token unless you pass key. 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
import { NoLag } from "@nolag/react-native";
const client = NoLag(tokenProvider, {
url: "wss://realtime.example.com/ws",
disconnectOnHidden: true,
});Off by default. With it on, the socket closes when the app goes to the background and reconnects on return to the foreground, which fires connect again, so subscriptions made in your connect handler come back. iOS reports inactive for transient states such as 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():
import { useEffect } from "react";
import { NoLag } from "@nolag/react-native";
function useNoLag() {
useEffect(() => {
const client = NoLag(tokenProvider, { url: "wss://realtime.example.com/ws" });
client.connect();
return () => client.destroy();
}, []);
}Options
Everything the JavaScript SDK's NoLagOptions accepts, plus:
| Option | Type | Default |
|---|---|---|
netInfo | NetInfoModule | none (no reachability signal) |
lifecycle | LifecycleAdapter | null | AppState adapter; null disables |
network | NetworkAdapter | null | built 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 these. Reach for them when your app-state signal does not come from AppState, when you use something other than NetInfo, or when you want the same decisions in 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 it, or null |
fromMMKV(mmkv) | Adapt a react-native-mmkv instance to TokenStore |
installTextCodecPolyfills() | Install the TextEncoder/TextDecoder polyfills if missing (done on import) |
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 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, so shipping it would bundle on Expo but fail the build on bare React Native. Audio 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 or later
@nolag/js-sdk1.12.0 or later (peer dependency)
Exactly one copy of the core. @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 a Blueprint SDK fail to typecheck with an unhelpful error. If that happens, add a resolutions (Yarn) or overrides (npm) entry pinning one version.