React Native
React Native / Expo Guide
Section titled “React Native / Expo Guide”atmosphere.js 5.0.22 supports React Native and Expo via the atmosphere.js/react-native subpath export. Tested baseline: Expo SDK 55, React Native 0.83, React 19.2 (see samples/spring-boot-ai-classroom/expo-client/package.json).
Installation
Section titled “Installation”bun add atmosphere.js
# Optional: for network-aware reconnectionbun add @react-native-community/netinfoQuick Start
Section titled “Quick Start”import NetInfo from '@react-native-community/netinfo';import { setupReactNative, AtmosphereProvider, useAtmosphereRN } from 'atmosphere.js/react-native';
// Call once at app startup — pass NetInfo for network-aware reconnectionsetupReactNative({ netInfo: NetInfo });
function Chat() { const { data, state, push, isConnected } = useAtmosphereRN({ request: { url: 'https://your-server.com/atmosphere/chat', transport: 'websocket', fallbackTransport: 'long-polling', }, });
return ( // your UI );}
export default function App() { return ( <AtmosphereProvider config={{ logLevel: 'info' }}> <Chat /> </AtmosphereProvider> );}setupReactNative()
Section titled “setupReactNative()”Call this once before any Atmosphere subscriptions are created. It:
- Installs a fetch-based EventSource polyfill (if the native one is missing)
- Detects ReadableStream support (Hermes on RN 0.73+ / Expo SDK 50+)
- Returns a capability report
import NetInfo from '@react-native-community/netinfo';
const caps = setupReactNative({ netInfo: NetInfo });// { hasReadableStream: true, hasWebSocket: true, recommendedTransports: ['websocket', 'streaming', 'long-polling'] }Without NetInfo:
const caps = setupReactNative();// Works fine — isConnected defaults to true, network-aware reconnection is disabledisReactNativeSetup()
Section titled “isReactNativeSetup()”Returns true once setupReactNative() has run. Useful in tests and when a library wants to guard against double initialization.
import { isReactNativeSetup, setupReactNative } from 'atmosphere.js/react-native';
if (!isReactNativeSetup()) { setupReactNative({ netInfo: NetInfo });}Important: Absolute URLs
Section titled “Important: Absolute URLs”React Native has no window.location. All Atmosphere request URLs must be absolute:
// Good{ url: 'https://example.com/atmosphere/chat', transport: 'websocket' }
// Bad - will throw an error in RN{ url: '/atmosphere/chat', transport: 'websocket' }useAtmosphereRN
Section titled “useAtmosphereRN”Drop-in replacement for useAtmosphere with React Native lifecycle integration.
const { data, state, push, isConnected, isInternetReachable } = useAtmosphereRN<Message>({ request: { url: 'https://example.com/chat', transport: 'websocket' }, backgroundBehavior: 'suspend', // 'suspend' | 'disconnect' | 'keep-alive'});Background behavior options:
suspend(default) — pauses the transport when app goes to background, resumes on foregrounddisconnect— fully closes the connection, reconnects on foregroundkeep-alive— does nothing, connection stays open
NetInfo integration (when passed to setupReactNative({ netInfo: NetInfo })):
isConnected/isInternetReachablereflect real network state- Connection is suspended when offline, resumed when back online
- Falls back to
{ isConnected: true, isInternetReachable: true }when NetInfo is not provided
useStreamingRN
Section titled “useStreamingRN”Drop-in replacement for useStreaming with the same RN lifecycle awareness.
const { fullText, isStreaming, isConnected, send, reset, close } = useStreamingRN({ request: { url: 'https://example.com/ai/chat', transport: 'websocket' },});Sends are suppressed when the device is offline.
useAtmosphereCore
Section titled “useAtmosphereCore”Shared low-level hook exported from atmosphere.js/react-native for building custom hooks. It manages subscription lifecycle, state, and cleanup — the RN-aware useAtmosphereRN and useStreamingRN hooks are built on top of it. Use it when the built-in hooks don’t fit your use case.
import { useAtmosphereCore } from 'atmosphere.js/react-native';
function useMyCustomHook(request) { const { state, push, atmosphere } = useAtmosphereCore({ request, handlers: { onOpen: () => {}, onMessage: (data) => {}, }, }); // ...}Core hooks (re-exported)
Section titled “Core hooks (re-exported)”These work as-is in React Native and are re-exported for convenience:
AtmosphereProvider/useAtmosphereContextuseRoomusePresence
Transport Compatibility
Section titled “Transport Compatibility”| Transport | Expo SDK 55 / RN 0.83 (tested) | Expo SDK 50+ / RN 0.73+ | RN < 0.73 | Notes |
|---|---|---|---|---|
| WebSocket | Full support | Full support | Full support | Primary transport |
| Long-Polling | Full support | Full support | Full support | Safe fallback (fetch only) |
| SSE | Native via polyfill | Via polyfill (streaming) | Via polyfill (text fallback) | Polyfill degrades to polling on old Hermes |
| Streaming | ReadableStream works | ReadableStream works | response.body is null | Skip on old Hermes |
setupReactNative() detects capabilities and reports recommended transports.
Metro Bundler Configuration
Section titled “Metro Bundler Configuration”When the app lives in a monorepo (e.g. samples/spring-boot-ai-classroom/expo-client/ with a file: dependency on the atmosphere.js sources) Metro needs a few tweaks so it can resolve the shared workspace without duplicating React:
const { getDefaultConfig } = require('expo/metro-config');const path = require('node:path');
const config = getDefaultConfig(__dirname);const workspaceRoot = path.resolve(__dirname, '../../..');
config.watchFolders = [workspaceRoot];config.resolver.nodeModulesPaths = [ path.resolve(__dirname, 'node_modules'), path.resolve(workspaceRoot, 'atmosphere.js/node_modules'),];config.resolver.extraNodeModules = new Proxy({}, { get: (_, name) => path.join(__dirname, `node_modules/${name}`),});config.resolver.blockList = [ new RegExp(`${workspaceRoot}/atmosphere.js/node_modules/react/.*`),];config.resolver.unstable_enablePackageExports = true;
module.exports = config;Top-level AppState import
Section titled “Top-level AppState import”The RN subpath uses a top-level import { AppState } from 'react-native'. Do not reach into esbuild/tsup’s synthetic require() helper — Metro cannot statically resolve wrapped require() calls. If you vendor the source, keep the static top-level import.
Explicit NetInfo injection
Section titled “Explicit NetInfo injection”@react-native-community/netinfo is not imported by atmosphere.js — you must pass it explicitly: setupReactNative({ netInfo: NetInfo }). This keeps NetInfo optional and avoids Metro static-analysis failures on optional dependencies.
Expo entry point
Section titled “Expo entry point”Expo Go requires registerRootComponent(App) at the entry. If you forget, the bundler builds but Expo Go never renders:
import { registerRootComponent } from 'expo';import App from './App';registerRootComponent(App);Known Limitations
Section titled “Known Limitations”- Hermes + ReadableStream: Hermes added ReadableStream support in RN 0.73 (Expo SDK 50). On older versions, SSE and streaming transports degrade or are unavailable.
- No
window.location: All URLs must be absolute. The WebSocket transport throws a clear error if given a relative URL withoutwindow.location. - NetInfo is optional: Pass it via
setupReactNative({ netInfo: NetInfo }). Without it,isConnecteddefaults totrueand network-aware reconnection is disabled. - Background audio: If you need to keep a connection alive while the app plays audio in the background, use
backgroundBehavior: 'keep-alive'.
Sample App
Section titled “Sample App”See samples/spring-boot-ai-classroom/expo-client/ for a complete Expo app that connects to the AI Classroom backend with 4 rooms (Math, Code, Science, General), streaming AI responses, and network status display.