@soasap-com/react-native-sdk
SOASAP React Native SDK
Lightweight, production-ready feature flags SDK for React Native 0.71+ and Expo — lock-free O(1) reads, local-first evaluation with AsyncStorage-backed cache, real-time SSE updates, reactive hooks, zero runtime dependencies, and graceful offline behavior.
Installation
Install from npm.
Requires React 18+, React Native 0.71+, and
@react-native-async-storage/async-storage 1.17+ as peer dependencies.
Expo: npx expo install @soasap-com/react-native-sdk @react-native-async-storage/async-storage
npm install @soasap-com/react-native-sdk @react-native-async-storage/async-storage react react-native
Quick start
Wrap your app with SoasapProvider, enable
preload: true for non-blocking startup sync,
and read flags with hooks that re-render when values change.
import { SoasapProvider, useSoasapBool } from '@soasap-com/react-native-sdk';
function App() {
return (
<SoasapProvider
options={{
apiKey: process.env.EXPO_PUBLIC_SOASAP_API_KEY!,
preload: true,
}}
>
<CheckoutScreen />
</SoasapProvider>
);
}
function CheckoutScreen() {
const newCheckout = useSoasapBool('new-checkout');
if (newCheckout) {
return <NewCheckout />;
}
return <LegacyCheckout />;
}
Imperative client (no hooks)
Use createSoasapClient() for sync reads outside React components
— navigation guards, utilities, or non-UI code paths.
import { createSoasapClient } from '@soasap-com/react-native-sdk';
const flags = createSoasapClient({
apiKey: process.env.EXPO_PUBLIC_SOASAP_API_KEY!,
preload: true,
});
if (flags.getBool('new-checkout')) {
console.log('New checkout enabled');
}
Typed access
Reactive hooks for screens and sync getters on the imperative client.
JSON keys from the dashboard are returned as stored (typically camelCase).
// Hooks (reactive)
useSoasapBool('feature-x');
useSoasapNumber('rate-limit', 100);
useSoasapString('ui-theme', 'light');
useSoasapJson<CheckoutConfig>('checkout-config');
// Client (sync)
flags.getBool('feature-x');
flags.getNumber('rate-limit', 100);
flags.getString('ui-theme', 'light');
flags.getJson<CheckoutConfig>('checkout-config');
Startup sync
preload: true (recommended) hydrates from AsyncStorage in the background
while the SSE worker connects — it never blocks your app shell render.
Without it, lazy mode defers the network connection until the first flag read.
Unlike the web SDK (localStorage is synchronous),
AsyncStorage.getItem() returns a Promise. On cold start,
getBool() and hooks still read in O(1) but may return defaults for ~20–50 ms
until the cache hydrates — use waitForStorage or
useSoasapStorageReady() to avoid a brief UI flash.
// Immediate sync (recommended)
<SoasapProvider options={{ apiKey: '...', preload: true }} />
// Gate until AsyncStorage is ready
<SoasapProvider
options={{ apiKey: '...', preload: true }}
waitForStorage
fallback={<SplashScreen />}
>
<App />
</SoasapProvider>
// Lazy sync — first read uses storage cache or defaults
<SoasapProvider options={{ apiKey: '...' }} />
Shared client instance
Create one client per app instance at module scope and pass it to the provider. The provider only auto-closes clients it creates itself.
Do not create the client inside a component without useMemo —
Fast Refresh can remount the root and leak SSE connections on every save.
const flags = createSoasapClient({
apiKey: process.env.EXPO_PUBLIC_SOASAP_API_KEY!,
preload: true,
});
export function Root() {
return (
<SoasapProvider client={flags}>
<Navigation />
</SoasapProvider>
);
}
Expo integration
Mount the provider at your app root. Works with Expo SDK 49+, bare React Native, and React Navigation.
// App.tsx
import { SoasapProvider } from '@soasap-com/react-native-sdk';
import { NavigationContainer } from '@react-navigation/native';
import { RootNavigator } from './navigation';
export default function App() {
return (
<SoasapProvider
options={{
apiKey: process.env.EXPO_PUBLIC_SOASAP_API_KEY!,
preload: true,
}}
>
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
</SoasapProvider>
);
}
Error handling & observability
Hook background diagnostics without affecting the hot path.
Sources: Network, Storage, Parser.
import { SoasapErrorSource, createSoasapClient } from '@soasap-com/react-native-sdk';
createSoasapClient({
apiKey: '...',
onError: (ctx) => {
console.error(
`[${ctx.source}] transient=${ctx.isTransient}`,
ctx.exception.message);
},
});
// SoasapErrorSource.Network | .Storage | .Parser
Production safety & guardrails
- Immutable snapshots — atomic reference swap; readers and hooks never see partial updates
- Memory cap protection (anti-DoS) — 5 MB SSE payload cap; oversized streams are dropped and reset
- Payload validation — root element must be a JSON object
{}; invalid payloads are ignored - Storage debounce — AsyncStorage writes coalesced at most once every ~2.5 seconds
- Zero runtime dependencies — React, React Native, and AsyncStorage are peer dependencies only
- One client per app — define
createSoasapClient()at module scope
Getters and hooks never throw (except missing SoasapProvider for hooks).
Offline resiliency
| Scenario | Behavior |
|---|---|
| API unavailable | Uses stale cached flags |
| SSE disconnected | Keeps last known snapshot |
| First visit without cache | Returns default values |
| Invalid payload | Payload ignored |
| Storage quota / failure | In-memory mode continues |
| Persistent network issues | Automatic reconnect with backoff |
AsyncStorage key: soasap:cache:<api-key-prefix>.
Override with cacheKeyPrefix: 'myapp:flags' or a custom storage implementation.
Architecture
[Hot Path] getBool() / hooks → currentSnapshot ref → O(1) lookup (sync, in-memory)
↑
| (atomic reference swap + subscribe)
[Background] AsyncStorage.getItem() → hydrate snapshot (~20–50 ms on cold start)
SSE → SseEventParser (5MB cap) → StorageWriteCoalescer → AsyncStorage
Supported environments
- React 18 / 19+
- React Native 0.71+ (Hermes recommended)
- Expo SDK 49+
@react-native-async-storage/async-storage1.17+fetchwithReadableStreamfor incremental SSE (Hermes highly recommended on Android)