SOASAP React SDK
Local-first feature flag evaluation for React 18+ — reactive hooks, browser cache, and real-time SSE updates.
Overview
The SOASAP React SDK synchronizes feature flag configuration from SOASAP Cloud in the background and evaluates flags from an in-memory snapshot. Flag reads are synchronous, O(1), and never perform network or storage I/O on the hot path. React hooks re-render components when the snapshot updates.
The SDK provides:
- Local evaluation — dictionary lookups against an immutable snapshot
- Reactive hooks —
useSoasapBool,useSoasapNumber, and related hooks viauseSyncExternalStore - Real-time synchronization — Server-Sent Events (SSE) delta updates
- Persistent cache —
localStorage-backed snapshot for return visits - Offline operation — last known snapshot when the network or control plane is unavailable
- Explicit defaults — every hook and getter accepts a code-defined fallback
- Non-blocking startup — optional
preload: truestarts sync without blocking render
Runtime characteristics
- O(1) local evaluation — reads from an in-memory snapshot; no network requests during evaluation
- Reactive updates — snapshot swaps notify subscribers; hooks re-render when flag values change
- Real-time SSE synchronization — background worker applies validated payloads with atomic configuration updates
- Persistent browser cache —
localStoragesnapshot for fast cold starts and return visits - Offline operation — last known snapshot when the network or control plane is unavailable
- Explicit defaults — missing, mistyped, or unsynced keys return your code-defined fallback
- Non-blocking startup —
preload: truestarts synchronization without blocking React render
SoasapProvider. Zero runtime dependencies — React is a peer dependency.
Requirements
Supported runtimes:
- React 18, 19+ (
react>= 18 peer dependency) - Modern browsers with
fetch,ReadableStream, andlocalStorage
Supported application types:
- Vite, Webpack, Create React App, and similar SPA bundlers
- Next.js App Router and Pages Router (client components only — mark with
'use client') - Single-page applications and client-rendered islands in hybrid apps
Requires outbound HTTPS to api.soasap.com for SSE synchronization from the browser.
Installation
npm:
npm install @soasap-com/react-sdk react
pnpm:
pnpm add @soasap-com/react-sdk react
yarn:
yarn add @soasap-com/react-sdk react
package.json dependency:
{
"dependencies": {
"@soasap-com/react-sdk": "1.0.3",
"react": "^18.0.0"
}
}
Verify installation
npm list @soasap-com/react-sdk
Confirm @soasap-com/react-sdk appears in the project dependency tree before continuing.
Before you begin
You will need:
- ✓ A SOASAP account
- ✓ A project
- ✓ An environment
- ✓ An environment API key
If you have not completed these steps, start with Quick Start.
Installation flow
After your project and environment exist in the dashboard, install the npm package and wrap your React tree
with SoasapProvider. The API key binds the browser session to a single
environment. Synchronization runs in the background; components evaluate flags locally via hooks or the imperative client.
Provider setup
Wrap your application root with SoasapProvider and enable preload for production SPAs:
import { SoasapProvider } from '@soasap-com/react-sdk';
const apiKey = import.meta.env.VITE_SOASAP_API_KEY;
if (!apiKey) {
throw new Error('VITE_SOASAP_API_KEY is required.');
}
export function Root() {
return (
<SoasapProvider
options={{
apiKey,
preload: true,
}}
>
<App />
</SoasapProvider>
);
}
- API key — environment-scoped credential sent as
X-API-Keyon SSE requests; identifies which configuration to synchronize - One client per tab — create a single client instance per browser tab or app shell
- Preload — with
preload: true, the SSE worker starts during provider mount without blocking React render - Auto cleanup — the provider calls
close()on internally created clients when it unmounts
Pass either options (provider creates the client) or a pre-built client from createSoasapClient.
Configuration
Inject the API key via build-time environment variables. The SDK does not read .env files directly — your bundler exposes them.
Recommended configuration
Development
- Vite:
.env.localwithVITE_SOASAP_API_KEY - Next.js:
.env.localwithNEXT_PUBLIC_SOASAP_API_KEY - Local env files excluded from version control
Production
- CI/CD build-time environment variables (
VITE_*orNEXT_PUBLIC_*) - Separate keys per deployment environment (Development, Staging, Production)
Never commit Production API keys to source control. Inject keys at build time through your deployment pipeline.
Vite .env.local:
VITE_SOASAP_API_KEY=soasap...
<SoasapProvider
options={{
apiKey: import.meta.env.VITE_SOASAP_API_KEY,
preload: true,
}}
/>
Shared client instance (recommended for testability and SSR setups):
import { SoasapProvider, createSoasapClient } from '@soasap-com/react-sdk';
const flags = createSoasapClient({
apiKey: import.meta.env.VITE_SOASAP_API_KEY!,
preload: true,
});
export function Root() {
return (
<SoasapProvider client={flags}>
<App />
</SoasapProvider>
);
}
Optional configuration:
import { SoasapErrorSource, createSoasapClient } from '@soasap-com/react-sdk';
createSoasapClient({
apiKey: import.meta.env.VITE_SOASAP_API_KEY!,
baseUrl: 'https://api.soasap.com',
cacheKeyPrefix: 'myapp:flags',
preload: true,
onError: (ctx) => {
console.warn(
`[SOASAP ${ctx.source}] transient=${ctx.isTransient}`,
ctx.exception.message,
);
},
});
Custom storage (for tests or sessionStorage): pass a storage object implementing getItem, setItem, and removeItem.
Evaluating flags
Use hooks in React components for reactive UI. Use the imperative client for event handlers, utilities, and non-UI code paths.
Hooks (reactive)
| Flag type | Hook | Default |
|---|---|---|
| Boolean | useSoasapBool | false |
| String | useSoasapString | '' |
| Number | useSoasapNumber | 0 |
| JSON | useSoasapJson<T> | undefined |
import { useSoasapBool, useSoasapNumber, useSoasapString, useSoasapJson } from '@soasap-com/react-sdk';
function CheckoutPage() {
const newCheckout = useSoasapBool('new-checkout', false);
const maxItems = useSoasapNumber('max-items', 10);
const theme = useSoasapString('checkout-theme', 'default');
const config = useSoasapJson<CheckoutConfig>('checkout-config', {
enableUpsells: false,
maxItems: 10,
});
if (!newCheckout) {
return <LegacyCheckout />;
}
return <NewCheckout maxItems={maxItems} theme={theme} config={config} />;
}
useSoasapFlag is an alias for useSoasapBool. useSoasapClient() returns the underlying client from context.
Imperative client (sync)
| Flag type | Method |
|---|---|
| Boolean | getBool |
| String | getString |
| Number | getNumber |
| JSON | getJson<T> |
const client = useSoasapClient();
function handleClick() {
if (client.getBool('beta-feature', false)) {
// ...
}
}
getFlag is an alias for getBool.
JSON
interface CheckoutConfig {
enableUpsells: boolean;
maxItems: number;
}
const config = useSoasapJson<CheckoutConfig>('checkout-config', {
enableUpsells: false,
maxItems: 10,
});
JSON keys from the dashboard are returned as stored (typically camelCase).
getJson returns a deep copy; non-object values or copy failures return the default.
Explicit defaults
Always pass a default on every hook and getter read:
useSoasapBool('new-checkout', false)
The default is returned when:
- The flag key does not exist in the synchronized snapshot
- The flag was deleted or disabled in the dashboard for this environment
- The page loads before the first SSE payload arrives and no browser cache exists
- A restored
localStoragecache is empty or invalid - The flag value type does not match the hook or getter (for example, a string flag read with
useSoasapBool) - Synchronization is temporarily delayed after deploy or reconnect
- During SSR — hooks use the server snapshot, which always returns your explicit default
Explicit defaults are your production safety net. They define safe behavior when configuration is missing, stale, or not yet synchronized — without error boundaries on the hot path.
false for feature gates and conservative values for limits.
Runtime architecture
- SSE delivers full or delta flag payloads to a background worker in the browser
- Valid payloads replace the in-memory snapshot atomically — readers never observe partial updates
- Snapshot swaps call
subscribelisteners; hooks viauseSyncExternalStorere-render affected components - Evaluation reads a local reference to an immutable snapshot with no locks on the render path
- No HTTP requests and no blocking storage I/O during hook evaluation or getter calls
Implementation details on the hot path:
- Snapshot replacement swaps an object reference — concurrent readers keep the previous snapshot until the swap completes
- Each getter copies the current snapshot reference locally before lookup
- SSE parser enforces a 5 MB payload cap; invalid root payloads are ignored
Persistent cache
On construction, the client synchronously loads the last persisted JSON snapshot from browser storage before any network activity. SSE updates enqueue debounced storage writes (coalesced every ~2.5 seconds).
Default storage key format:
soasap:cache:<api-key-prefix>
The prefix is derived from the first 10 characters of the API key (after stripping a leading soasap literal). Override the prefix:
createSoasapClient({
apiKey: '...',
cacheKeyPrefix: 'myapp:flags',
});
When localStorage is unavailable (SSR, private browsing quota exceeded, or tests), the client continues in memory-only mode.
Return visits and reloads — the UI hydrates from localStorage immediately; SSE reconnects in the background. First visit without cache returns defaults until the first successful sync.
Offline operation
The SDK continues evaluating flags from the last known snapshot when:
- Internet connectivity is unavailable
- SOASAP Cloud returns errors or is unreachable
- The SSE connection drops (proxy timeout, firewall, transient network failure)
The background worker reconnects with exponential backoff. Reads are unaffected during disconnect — only future updates are delayed.
Stale configuration is preferable to unavailable configuration: the application keeps running with the last known values and your code defaults.
| Scenario | Behavior |
|---|---|
| API unavailable | Serves stale cached snapshot |
| SSE disconnected | Keeps last known snapshot; reconnects with backoff |
| First visit, no cache | Returns explicit defaults until first sync |
| Invalid SSE payload | Payload ignored; snapshot unchanged |
| Storage quota / failure | In-memory mode continues; error reported via onError |
Preload and startup behavior
preload: true is recommended for production SPAs and client-rendered apps.
With preload:
- Client construction loads the browser cache synchronously
- The SSE worker starts immediately — no blocking during provider mount or first render
- The app shell renders while synchronization completes in the background
- First paint may use cached flags or explicit defaults until the first SSE payload arrives
Without preload (lazy mode):
<SoasapProvider options={{ apiKey }} />
The SSE worker starts on the first flag read (hook render or getter call). Until then, evaluation uses the browser cache (if present) or your explicit defaults. Prefer preload for predictable synchronization timing after deploy.
SSR and hydration
Hooks use useSyncExternalStore with a server snapshot that always returns your explicit default.
During SSR, components render with defaults — not live flag values from the network.
After hydration on the client, the provider loads localStorage, connects SSE, and hooks
re-render when the snapshot updates. Avoid layout shift by designing defaults that match your loading state,
or defer flag-dependent UI until after client mount.
For Next.js, wrap client components with SoasapProvider at the layout level. See Next.js Integration and SSR.
Graceful shutdown
Call close() to cancel SSE, flush storage (up to 5 seconds), and release resources:
await flags.close();
SoasapProvider calls close() automatically for clients it creates internally on unmount. When passing a shared client prop, you are responsible for calling close() if the client outlives the provider.
Best practices
- Wrap at the root — mount
SoasapProvideronce near the top of your component tree. - One client per tab — single client instance per browser tab or app shell; do not create clients per route or component.
- Enable preload —
preload: truefor non-blocking background sync at startup. - Always pass explicit defaults — safe behavior when keys are missing or not yet synchronized.
- Use hooks in components — reactive UI; use
useSoasapClient()getters only in event handlers or effects. - Separate keys per environment — Development, Staging, and Production hold independent configuration.
- Inject keys at build time — see Recommended configuration.
- Do not nest multiple providers — avoids duplicate SSE connections unless intentionally isolating subtrees.
Common issues
Hook throws "must be used within a SoasapProvider"
- Ensure the component calling the hook is rendered inside
SoasapProvider - In Next.js, confirm the file is a client component (
'use client') and the provider wraps the tree above it
Flag always returns the default
- Verify the API key matches the intended environment
- Confirm the flag key exists and is enabled in that environment
- Check the flag type matches the hook (
useSoasapBoolfor boolean flags)
Dashboard changes do not appear
- Verify outbound HTTPS to
api.soasap.comand SSE is not blocked by a proxy, firewall, or browser extension - See SSE Disconnected
Wrong value returned
- Verify the flag value in the selected environment — Development and Production are independent
- Confirm the deployed build uses the API key for that environment (check build-time env vars)
Multiple SDK instances
Symptoms
- Multiple SSE connections from the same browser tab
- Increased memory usage
- Duplicate synchronization traffic
Resolution — mount one SoasapProvider at the app root and reuse the same client. Do not call createSoasapClient inside individual components or per-route layouts.
Startup synchronization delays
Expected with preload: the app renders immediately; the first SSE payload may arrive seconds after load. Until then, evaluation uses the browser cache or explicit defaults. This is by design — startup is never blocked on sync.
Complete example
The following example demonstrates the recommended production setup for Vite + React applications, including environment configuration, provider setup, preload, and reactive flag evaluation.
// main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { SoasapProvider } from '@soasap-com/react-sdk';
import { App } from './App';
const apiKey = import.meta.env.VITE_SOASAP_API_KEY;
if (!apiKey) {
throw new Error('Set VITE_SOASAP_API_KEY in .env.local');
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<SoasapProvider
options={{
apiKey,
preload: true,
}}
>
<App />
</SoasapProvider>
</StrictMode>,
);
// CheckoutPage.tsx
import {
useSoasapBool,
useSoasapNumber,
useSoasapString,
} from '@soasap-com/react-sdk';
export function CheckoutPage() {
const newCheckout = useSoasapBool('new-checkout', false);
const maxItems = useSoasapNumber('max-items', 10);
const theme = useSoasapString('checkout-theme', 'default');
if (!newCheckout) {
return <LegacyCheckout />;
}
return (
<NewCheckout maxItems={maxItems} theme={theme} />
);
}
Production recommendations
- Inject API keys at build time via CI/CD — never hardcode Production keys in source
- Use separate API keys per environment — never share Production keys with local Development builds
- Treat client-side API keys as public credentials scoped to a single environment
- Keep explicit defaults in code for every flag read — defaults define behavior during outages and SSR
- Register
onErrorin Production to log background sync, storage, and parser failures without affecting reads - Mount the provider once at the root; avoid duplicate clients per tab
- Monitor browser connectivity and SSE health in Production user sessions where possible
Next steps
- Feature Flags — flag types, defaults, and naming
- Architecture — local evaluation, SSE, cache, and offline operation
- API Keys — issuing, rotating, and securing environment keys
- Other SDKs — .NET, Node.js, Python, and more
Deep dives: Provider, Hooks, Offline Behavior.