SignaKitdocs
SDKs

React Native

Full API reference for @signakit/flags-react-native — SignaKitProvider, useFlag hook, and AsyncStorage config persistence for feature flags in React Native and Expo apps.

React Native SDK

Package: @signakit/flags-react-native Registry: npm Environments: React Native ≥ 0.74, Expo SDK 51+

A self-contained SDK for React Native and Expo apps. Fetches your flag config from the SignaKit CDN once on initialization, evaluates all flags locally on-device, and surfaces decisions through a context provider and a useFlag hook. No browser APIs are used — events go over fetch, and optional config persistence uses AsyncStorage rather than sessionStorage.

Building a web React app? Use @signakit/flags-react instead — it targets the browser and pairs with @signakit/flags-browser. See the React SDK →


Installation

npm install @signakit/flags-react-native

If you want offline-tolerant cold starts, also install the optional AsyncStorage peer:

npm install @react-native-async-storage/async-storage

For Expo managed workflow, link the native module:

npx expo install @react-native-async-storage/async-storage

SignaKitProvider

Wrap your app root with SignaKitProvider. It creates the SDK client, fetches the flag config, and makes all decisions available to any useFlag call in the tree.

App.tsx
import { SignaKitProvider } from '@signakit/flags-react-native'
import { RootNavigator } from './navigation/RootNavigator'

export default function App() {
  return (
    <SignaKitProvider
      sdkKey="sk_prod_yourOrgId_yourProjectId_random"
      userId="user-123"
      attributes={{ plan: 'pro', country: 'US' }}
      persistConfig
      loadingFallback={<SplashScreen />}
    >
      <RootNavigator />
    </SignaKitProvider>
  )
}

Props

PropTypeRequiredDescription
sdkKeystringYour SignaKit browser SDK key
userIdstringStable unique identifier for the current user. Changes trigger re-evaluation of all flags.
attributesUserAttributesUser attributes matched against targeting rules. Supports string, number, boolean, and string[] values.
persistConfigbooleanWhen true, the last successfully fetched config is written to AsyncStorage so the app can boot offline. Requires @react-native-async-storage/async-storage. Defaults to false.
loadingFallbackReactNodeRendered while the client is initializing. Defaults to null (children render with loading: true from useFlag).

Memoize attributes to avoid unnecessary re-evaluation

// ❌ New object on every render → recreates user context and re-evaluates all flags
<SignaKitProvider attributes={{ plan: user.plan }}>

// ✓ Stable reference
const attributes = useMemo(() => ({ plan: user.plan }), [user.plan])
<SignaKitProvider attributes={attributes}>

Initialization behavior

The Provider runs this sequence on mount:

  1. If persistConfig is enabled, loads the last cached config from AsyncStorage immediately.
  2. Fetches fresh config from the SignaKit CDN.
  3. If the network request succeeds, the fresh config is used and written back to AsyncStorage (when persistConfig is enabled).
  4. If the network request fails but a cached config was loaded in step 1, the Provider resolves as ready using the cached config — the app boots offline-tolerant.
  5. If both fail, the Provider resolves with loading: false and all useFlag calls return the off state (fail-open).

useFlag(flagKey)

Evaluates a single flag for the current user.

import { useFlag } from '@signakit/flags-react-native'

function CheckoutButton() {
  const { enabled, variationKey, loading } = useFlag('new-checkout')

  if (loading) return <ActivityIndicator />

  return enabled ? <NewCheckoutButton /> : <LegacyCheckoutButton />
}

Return value

FieldTypeDescription
enabledbooleanWhether the flag is on for this user. false while loading.
variationKeystringAssigned variation key (e.g. 'on', 'off', 'treatment'). 'off' while loading or when the flag is disabled.
ruleKeystring | nullThe rule key that matched this user. null for the default allocation or while loading.
ruleType'ab-test' | 'multi-armed-bandit' | 'targeted' | nullThe rule type that produced this decision. null for the default allocation, disabled flags, or while loading.
variablesRecord<string, VariableValue>Resolved variable values for the matched variation. Empty object while loading or when no variables are defined.
loadingbooleantrue until the Provider finishes initializing. Always false when loadingFallback is set on the Provider.

Variation-specific rendering

Use variationKey when you need to render different UI for each variant of an A/B test:

function HeroSection() {
  const { variationKey, loading } = useFlag('hero-redesign')

  if (loading) return <Skeleton />

  if (variationKey === 'treatment_a') return <HeroVariantA />
  if (variationKey === 'treatment_b') return <HeroVariantB />
  return <HeroControl />
}

Flag variables

Flags can carry typed variable payloads — use variables to read them:

function OnboardingFlow() {
  const { enabled, variables } = useFlag('onboarding-steps')

  if (!enabled) return <DefaultOnboarding />

  const maxSteps = variables['max_steps'] as number ?? 3
  return <OnboardingWizard steps={maxSteps} />
}

useFlagAll()

useFlagAll() does not exist in this SDK. To evaluate multiple flags, call useFlag() individually for each key. This keeps exposure tracking precise and avoids firing $exposure events for flags the user never actually encountered.

If you need to evaluate all flags at once outside of React (e.g., in a navigation guard or background task), use the client directly:

import { useUserContext } from '@signakit/flags-react-native'

function useAllDecisions() {
  const userContext = useUserContext()
  if (!userContext) return {}
  return userContext.decideAll()
}

decideAll() fires an $exposure event for every flag the user is bucketed into. Call it once, not on every render.


useTrackEvent()

There is no useTrackEvent hook in this SDK. Access trackEvent through the user context directly.

import { useUserContext } from '@signakit/flags-react-native'

function PurchaseButton({ amount }: { amount: number }) {
  const userContext = useUserContext()

  const handlePress = async () => {
    await userContext?.trackEvent('purchase_completed', {
      value: amount,
      metadata: { plan: 'pro' },
    })
  }

  return <Button onPress={handlePress} title="Buy now" />
}

trackEvent accepts an event key and an optional options object:

OptionTypeDescription
valuenumberNumeric value for revenue or goal metrics
metadataRecord<string, unknown>Arbitrary metadata attached to the event. Dropped silently if serialized size exceeds 5 000 bytes.

Fire and forget — trackEvent never throws. Errors are logged to the console.


useUserContext()

Returns the active SignaKitUserContext instance, or null while the Provider is initializing. Use this when you need direct access to decide(), decideAll(), or trackEvent() outside of the useFlag hook.

import { useUserContext } from '@signakit/flags-react-native'

function FeatureGate({ flagKey, children }: { flagKey: string; children: ReactNode }) {
  const userContext = useUserContext()
  if (!userContext) return null

  const decision = userContext.decide(flagKey)
  return decision?.enabled ? <>{children}</> : null
}

AsyncStorage persistence

When persistConfig is enabled on the Provider, the SDK writes the latest successful config to AsyncStorage under the key prefix @signakit/config:. On the next cold start, this cached config is loaded immediately before the network request completes — so the app renders with real flag decisions rather than the off/loading state.

AsyncStorage keys written by the SDK:
  @signakit/config:{sdkKey}   — the serialized flag config JSON
  @signakit/etag:{sdkKey}     — the ETag of the last fetch for conditional requests

The onReady() result includes a fromCache boolean indicating whether the ready state was satisfied by the AsyncStorage cache rather than a fresh network response:

const { success, fromCache, reason } = await client.onReady()
// success: true, fromCache: true  → served from cache; network request may still be pending
// success: true, fromCache: false → fresh config fetched successfully
// success: false                  → no network and no cache available

Expo Go supports AsyncStorage out of the box. For bare React Native, run npx pod-install after installing the peer package to link the native module.


Exposure deduplication

Unlike the browser SDK, there is no sessionStorage available in React Native. The SDK uses an in-memory Map keyed by ${flagKey}:${userId} to deduplicate $exposure events. The map lives for the lifetime of the SignaKitClient instance — it resets on app cold start, which mirrors the natural session boundary of a mobile app.

$exposure events are only fired for ab-test and multi-armed-bandit rules. Flags using targeted rules (simple rollouts) never fire an exposure event, since there is no experiment to attribute.


TypeScript types

import type {
  SignaKitClientConfig,
  OnReadyResult,
  UserAttributes,
  SignaKitDecision,
  SignaKitDecisions,
  SignaKitEvent,
  TrackEventOptions,
  VariableValue,
  FlagVariable,
  RuleType,
  Environment,
  AsyncStorageLike,
} from '@signakit/flags-react-native'

The AsyncStorageLike interface describes the minimal contract the SDK requires for persistence. Pass a custom implementation if you prefer a different storage backend:

export interface AsyncStorageLike {
  getItem(key: string): Promise<string | null>
  setItem(key: string, value: string): Promise<void>
  removeItem(key: string): Promise<void>
}

Differences from the web React SDK

Concern@signakit/flags-react (web)@signakit/flags-react-native
Session persistencesessionStorage (browser)In-memory Map (resets on cold start)
Config persistenceNot supportedAsyncStorage via persistConfig: true
Event transportnavigator.sendBeaconfetch fallbackfetch only (Hermes/RN runtime)
Bot detectionAuto-detected via $userAgentNot applicable — pass $userAgent explicitly if needed
FlagGate componentIncludedNot included — use useFlag or useUserContext
useFlagAll hookIncludedNot included — call userContext.decideAll() directly
useTrackEvent hookIncludedNot included — call userContext.trackEvent() directly
Peer dependency@signakit/flags-browserNone (self-contained)

Anti-patterns

PatternProblemFix
Inline attributes object on the ProviderNew object reference on every render recreates the user context and re-evaluates all flagsMemoize with useMemo
Using @signakit/flags-browser in a React Native projectThe browser SDK calls sessionStorage, navigator, and DOM APIs that do not exist in the React Native runtimeUse @signakit/flags-react-native instead
Calling userContext.decide() directly inside a render function without memoizationEvaluation runs on every render; bypasses the loading guard in useFlagUse the useFlag hook
Calling userContext.decideAll() on every screen mountFires a fresh $exposure set on every navigation eventCall once after onReady() and cache the result in state or context
Setting persistConfig without installing the AsyncStorage peertryLoadAsyncStorage() returns null silently; the SDK falls back to no persistence without errorInstall @react-native-async-storage/async-storage and link it before enabling persistConfig
Passing a different userId prop without memoizing attributesThe user context is recreated when userId changes; if attributes is also a new object it triggers a second recreation on the same renderKeep attributes memoized with useMemo whenever userId can change

Last updated on

On this page