@contentful/optimization-react-native
v0.1.0-alpha12
Published
<p align="center"> <a href="https://www.contentful.com/developers/docs/personalization/"> <img alt="Contentful Logo" title="Contentful" src="https://raw.githubusercontent.com/contentful/optimization/v0.1.0-alpha12/contentful-icon.png" width="150">
Downloads
309
Keywords
Readme
Guides · Reference · Contributing
[!WARNING]
The Optimization SDK Suite is pre-release (alpha). Breaking changes may be published at any time.
The Optimization React Native SDK implements functionality specific to React Native applications, based on the Optimization Core Library. This SDK is part of the Contentful Optimization SDK Suite.
- Getting Started
- Reference Implementation
- Configuration
- Entry Tracking
- Screen Tracking
- OptimizationRoot
- Live Updates Behavior
- React Native-Specific Defaults
- Offline Support
- Polyfills
Getting Started
Install using an NPM-compatible package manager, pnpm for example:
pnpm install @contentful/optimization-react-native @react-native-async-storage/async-storageFor offline support (recommended), also install:
pnpm install @react-native-community/netinfoImport the Optimization React Native SDK; both CJS and ESM module systems are supported, ESM preferred:
import { ContentfulOptimization } from '@contentful/optimization-react-native'Configure and initialize the Optimization React Native SDK:
const optimization = await ContentfulOptimization.create({
clientId: 'your-client-id',
environment: 'main',
})Reference Implementation
- React Native: Example application that displays optimized content, with builds targeted for both Android and iOS
Configuration
The SDK communicates with two APIs: the Experience API (for optimization selection and variant resolution) and the Insights API (for event ingestion).
Top-level Configuration Options
| Option | Required? | Default | Description |
| ------------------- | --------- | --------------------------- | ----------------------------------------------------------------------- |
| allowedEventTypes | No | ['identify', 'screen'] | Allow-listed event types permitted when consent is not set |
| api | No | See "API Options" | Unified configuration for the Experience API and Insights API endpoints |
| clientId | Yes | N/A | The Optimization client identifier |
| defaults | No | undefined | Set of default state values applied on initialization |
| environment | No | 'main' | The environment identifier |
| eventBuilder | No | See "Event Builder Options" | Event builder configuration (channel/library metadata, etc.) |
| fetchOptions | No | See "Fetch Options" | Configuration for Fetch timeout and retry functionality |
| getAnonymousId | No | undefined | Function used to obtain an anonymous user identifier |
| logLevel | No | 'error' | Minimum log level for the default console sink |
| onEventBlocked | No | undefined | Callback invoked when an event call is blocked by guards |
| queuePolicy | No | See "Queue Policy Options" | Shared queue and retry configuration for stateful delivery |
Configuration method signatures:
getAnonymousId:() => string | undefined
API Options
| Option | Required? | Default | Description |
| ------------------- | --------- | ------------------------------------------ | ------------------------------------------------------------------------------ |
| experienceBaseUrl | No | 'https://experience.ninetailed.co/' | Base URL for the Experience API |
| insightsBaseUrl | No | 'https://ingest.insights.ninetailed.co/' | Base URL for the Insights API |
| beaconHandler | No | undefined | Custom handler used to enqueue Insights API event batches |
| enabledFeatures | No | ['ip-enrichment', 'location'] | Enabled features the Experience API may use for each request |
| ip | No | undefined | IP address override used by the Experience API for location analysis |
| locale | No | 'en-US' (in API) | Locale used to translate location.city and location.country |
| plainText | No | false | Sends performance-critical Experience API endpoints in plain text |
| preflight | No | false | Instructs the Experience API to aggregate a new profile state but not store it |
Configuration method signatures:
beaconHandler:(url: string | URL, data: BatchInsightsEventArray) => boolean
Event Builder Options
Event builder options should only be supplied when building an SDK on top of the Optimization React Native SDK or any of its descendant SDKs.
| Option | Required? | Default | Description |
| ------------------- | --------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| app | No | undefined | The application definition used to attribute events to a specific consumer app |
| channel | No | 'mobile' | The channel that identifies where events originate from (e.g. 'web', 'mobile') |
| library | No | { name: '@contentful/optimization-react-native', version: '0.0.0' } | The client library metadata that is attached to all events |
| getLocale | No | Built-in locale resolution | Function used to resolve the locale for outgoing events |
| getPageProperties | No | Built-in page properties resolution | Function that returns the current page properties |
| getUserAgent | No | Built-in user agent resolution | Function used to obtain the current user agent string when applicable |
The channel option may contain one of the following values:
webmobileserver
Configuration method signatures:
getLocale:() => string | undefinedgetPageProperties:() => { path: string, query: Record<string, string>, referrer: string, search: string, title?: string, url: string }getUserAgent:() => string | undefined
Fetch Options
Fetch options allow for configuration of a Fetch API-compatible fetch method and the retry/timeout
logic integrated into the SDK's bundled API clients. Specify the fetchMethod when the host
application environment does not offer a fetch method that is compatible with the standard Fetch
API in its global scope.
| Option | Required? | Default | Description |
| ------------------ | --------- | ----------- | --------------------------------------------------------------------- |
| fetchMethod | No | undefined | Signature of a fetch method used by the API clients |
| intervalTimeout | No | 0 | Delay (in milliseconds) between retry attempts |
| onFailedAttempt | No | undefined | Callback invoked whenever a retry attempt fails |
| onRequestTimeout | No | undefined | Callback invoked when a request exceeds the configured timeout |
| requestTimeout | No | 3000 | Maximum time (in milliseconds) to wait for a response before aborting |
| retries | No | 1 | Maximum number of retry attempts |
Configuration method signatures:
fetchMethod:(url: string | URL, init: RequestInit) => Promise<Response>onFailedAttemptandonRequestTimeout:(options: FetchMethodCallbackOptions) => void
Queue Policy Options
queuePolicy is available in the stateful React Native SDK runtime and combines shared flush retry
settings with Experience API offline buffering controls.
Configuration shape:
{
flush?: {
baseBackoffMs?: number,
maxBackoffMs?: number,
jitterRatio?: number,
maxConsecutiveFailures?: number,
circuitOpenMs?: number,
onFlushFailure?: (context: QueueFlushFailureContext) => void,
onCircuitOpen?: (context: QueueFlushFailureContext) => void,
onFlushRecovered?: (context: QueueFlushRecoveredContext) => void
},
offlineMaxEvents?: number,
onOfflineDrop?: (context: ExperienceQueueDropContext) => void
}Supporting callback payloads:
type ExperienceQueueDropContext = {
droppedCount: number
droppedEvents: ExperienceEventArray
maxEvents: number
queuedEvents: number
}
type QueueFlushFailureContext = {
consecutiveFailures: number
queuedBatches: number
queuedEvents: number
retryDelayMs: number
}
type QueueFlushRecoveredContext = {
consecutiveFailures: number
}Notes:
flushapplies the same retry/backoff/circuit policy to both Insights API flushing and Experience API offline replay.- Invalid numeric values fall back to defaults.
jitterRatiois clamped to[0, 1].maxBackoffMsis normalized to be at leastbaseBackoffMs.- Failed flush attempts include both
falseresponses and thrown send errors.
[!IMPORTANT]
Call
ContentfulOptimization.create(...)once per app runtime and share the returned instance. In tests or hot-reload workflows, calldestroy()before creating a replacement instance.
Entry Tracking
Entry tracking refers to tracking Contentful entries (content entries in your CMS) for analytics purposes — views, taps, and variant resolution — not React Native UI components.
<OptimizedEntry />
A unified component that handles both optimized and non-optimized Contentful entries. It automatically:
- Detects whether the entry is optimized (has
nt_experiencesfield) - Resolves the correct variant for optimized entries based on user profile
- Passes non-optimized entries through unchanged
- Tracks entry views when visibility and time thresholds are met
- Tracks taps when enabled
children accepts either a render prop (resolvedEntry) => ReactNode for accessing the
resolved entry, or static children ReactNode for tracking-only use cases:
{
/* Render prop — receives the resolved entry (variant or baseline) */
}
;<OptimizedEntry entry={optimizedEntry}>
{(resolvedEntry) => <HeroComponent data={resolvedEntry.fields} />}
</OptimizedEntry>
{
/* Static children — tracking only, no variant resolution needed */
}
;<OptimizedEntry entry={productEntry}>
<ProductCard data={productEntry.fields} />
</OptimizedEntry>The component tracks when an entry:
- Is at least 80% visible in the viewport (configurable via
thresholdprop) - Has been viewed for 2000ms (2 seconds, configurable via
viewTimeMsprop)
With vs Without OptimizationScrollProvider
The tracking component works in two modes:
With OptimizationScrollProvider (Recommended for Scrollable Content)
When used inside a <OptimizationScrollProvider>, tracking uses the actual scroll position and
viewport dimensions:
<OptimizationScrollProvider>
<OptimizedEntry entry={optimizedEntry}>
{(resolvedEntry) => <HeroComponent data={resolvedEntry} />}
</OptimizedEntry>
<OptimizedEntry entry={productEntry}>
<ProductCard data={productEntry.fields} />
</OptimizedEntry>
</OptimizationScrollProvider>Benefits:
- Accurate viewport tracking as user scrolls
- Works for content that appears below the fold
- Triggers when an entry scrolls into view
Without OptimizationScrollProvider (For Non-Scrollable Content)
When used without <OptimizationScrollProvider>, tracking uses screen dimensions instead:
<OptimizedEntry entry={entry}>
{(resolvedEntry) => <FullScreenHero data={resolvedEntry} />}
</OptimizedEntry>
<OptimizedEntry entry={bannerEntry}>
<Banner data={bannerEntry.fields} />
</OptimizedEntry>Note: In this mode, scrollY is always 0 and viewport height equals the screen height. This
is ideal for:
- Full-screen components
- Non-scrollable layouts
- Content that's always visible when the screen loads
Custom Tracking Thresholds
<OptimizedEntry /> supports customizable visibility and time thresholds:
{/* Optimized entry with custom thresholds */}
<OptimizedEntry
entry={entry}
viewTimeMs={3000} // Track after 3 seconds of visibility
threshold={0.9} // Require 90% visibility
>
{(resolvedEntry) => <YourComponent data={resolvedEntry.fields} />}
</OptimizedEntry>
{/* Non-optimized entry with custom thresholds */}
<OptimizedEntry
entry={entry}
viewTimeMs={1500} // Track after 1.5 seconds
threshold={0.5} // Require 50% visibility
>
<YourComponent />
</OptimizedEntry>Key Features:
- The initial view event fires once per component mount; periodic duration updates continue while visible
- Works with or without
OptimizationScrollProvider(automatically adapts) - Default: 80% visible for 2000ms (both configurable)
- Tracking fires even if user never scrolls (checks on initial layout)
- Render prop pattern provides the resolved entry; static children work for tracking only
Manual Analytics Tracking
For cases outside the <OptimizedEntry /> component pattern — such as custom screens or
non-Contentful content — you can manually track events using the Insights API methods exposed by the
SDK:
import { useOptimization } from '@contentful/optimization-react-native'
function MyComponent() {
const optimization = useOptimization()
const trackManually = async () => {
await optimization.trackView({
componentId: 'entry-123',
experienceId: 'exp-456',
variantIndex: 0,
})
}
return <Button onPress={trackManually} title="Track" />
}useInteractionTracking
Returns the resolved interaction tracking configuration from the nearest
InteractionTrackingProvider (set up by OptimizationRoot). Use this to check whether view or tap
tracking is globally enabled:
import { useInteractionTracking } from '@contentful/optimization-react-native'
function DebugOverlay() {
const { views, taps } = useInteractionTracking()
return (
<Text>
Views: {String(views)}, Taps: {String(taps)}
</Text>
)
}useTapTracking
Low-level hook that detects taps on a View via raw touch events and emits entry tap events (wire
type component_click). <OptimizedEntry /> uses this internally, but you can use it directly for
custom tracking layouts:
import { useTapTracking } from '@contentful/optimization-react-native'
function TrackedEntry({ entry }: { entry: Entry }) {
const { onTouchStart, onTouchEnd } = useTapTracking({
entry,
enabled: true,
})
return (
<View onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
<Text>{entry.fields.title}</Text>
</View>
)
}Screen Tracking
useScreenTracking
Hook for tracking screen views. By default, tracks the screen automatically when the component
mounts. Set trackOnMount: false to disable automatic tracking and use the returned trackScreen
function for manual control.
import { useScreenTracking } from '@contentful/optimization-react-native'
// Automatic tracking on mount (default)
function HomeScreen() {
useScreenTracking({ name: 'Home' })
return <View>...</View>
}
// Manual tracking
function DetailsScreen() {
const { trackScreen } = useScreenTracking({
name: 'Details',
trackOnMount: false,
})
useEffect(() => {
if (dataLoaded) {
trackScreen()
}
}, [dataLoaded])
return <View>...</View>
}useScreenTrackingCallback
Returns a stable callback to track screen views with dynamic names. Use this when screen names are not known at render time (e.g., from navigation state):
import { useScreenTrackingCallback } from '@contentful/optimization-react-native'
function DynamicScreen({ screenName }: { screenName: string }) {
const trackScreenView = useScreenTrackingCallback()
useEffect(() => {
trackScreenView(screenName, { source: 'deep-link' })
}, [screenName])
return <View>...</View>
}OptimizationNavigationContainer
Wraps React Navigation's NavigationContainer to automatically track screen views when the active
route changes. Uses a render prop pattern so navigation props are spread onto the
NavigationContainer without requiring a direct dependency on @react-navigation/native:
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import {
OptimizationNavigationContainer,
OptimizationProvider,
} from '@contentful/optimization-react-native'
const Stack = createNativeStackNavigator()
function App() {
return (
<OptimizationProvider instance={optimization}>
<OptimizationNavigationContainer>
{(navigationProps) => (
<NavigationContainer {...navigationProps}>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
)}
</OptimizationNavigationContainer>
</OptimizationProvider>
)
}| Prop | Required? | Default | Description |
| --------------- | --------- | ------- | ----------------------------------------------------------------- |
| children | Yes | N/A | Render prop receiving ref, onReady, and onStateChange |
| onStateChange | No | N/A | Called when navigation state changes, after screen tracking fires |
| onReady | No | N/A | Called when navigation container is ready, after initial tracking |
| includeParams | No | false | Whether to include route params in screen event properties |
OptimizationRoot
OptimizationRoot is the recommended way to set up the SDK. It combines OptimizationProvider with
optional preview panel functionality:
import { OptimizationRoot, OptimizationScrollProvider } from '@contentful/optimization-react-native'
import { createClient } from 'contentful'
const contentfulClient = createClient({
space: 'your-space-id',
accessToken: 'your-access-token',
})
function App() {
return (
<OptimizationRoot
clientId="your-client-id"
environment="your-environment"
previewPanel={{
enabled: __DEV__, // Only show in development
contentfulClient: contentfulClient,
}}
>
<OptimizationScrollProvider>{/* Your app content */}</OptimizationScrollProvider>
</OptimizationRoot>
)
}OptimizationRoot also accepts a trackEntryInteraction prop to control global view and tap
tracking for all <OptimizedEntry /> components. By default, view tracking is enabled and tap
tracking is disabled:
<OptimizationRoot
clientId="your-client-id"
environment="your-environment"
trackEntryInteraction={{ views: true, taps: true }}
>
{/* All OptimizedEntry components will track both views and taps */}
</OptimizationRoot>Individual <OptimizedEntry /> components can override the global setting via their trackViews
and trackTaps props.
Preview Panel
When previewPanel.enabled is true, a floating action button appears that opens the preview
panel. The panel allows developers to:
- Browse and override audience membership
- Select specific variants for experiences
- View current profile information
- Test optimizations without modifying actual user data
[!IMPORTANT]
The React Native preview panel is intentionally tightly coupled to Core preview internals. It uses symbol-keyed
registerPreviewPanel(...)bridge access, direct signal updates, and state interceptors by design to apply immediate local overrides and keep preview behavior aligned with the Web preview panel.
<OptimizationRoot
clientId="your-client-id"
environment="your-environment"
previewPanel={{
enabled: true,
contentfulClient: contentfulClient,
fabPosition: { bottom: 50, right: 20 }, // Optional: customize button position
showHeader: true, // Optional: show header in panel
onVisibilityChange: (isVisible) => {
console.log('Preview panel visible:', isVisible)
},
}}
>
{/* ... */}
</OptimizationRoot>Live Updates Behavior
By default, <OptimizedEntry /> components lock to the first variant they receive. This
prevents UI "flashing" when user actions (like identifying or taking actions that change audience
membership) cause them to qualify for different optimizations mid-session.
Default Behavior (Recommended)
// User sees Variant A on initial load
<OptimizedEntry entry={heroEntry}>
{(resolvedEntry) => <Hero data={resolvedEntry.fields} />}
</OptimizedEntry>
// Even if the user later qualifies for Variant B (e.g., after identify()),
// they continue to see Variant A until the component unmountsThis provides a stable user experience where content doesn't unexpectedly change while the user is viewing it.
Enabling Live Updates
There are three ways to enable live updates (immediate reactions to optimization changes):
1. Preview Panel (Automatic)
When the preview panel is open, all <OptimizedEntry /> components automatically enable live
updates. This allows developers to test different variants without refreshing the screen:
<OptimizationRoot
clientId="your-client-id"
environment="your-environment"
previewPanel={{ enabled: true, contentfulClient }}
>
{/* All OptimizedEntry components will live-update when panel is open */}
</OptimizationRoot>2. Global Setting via OptimizationRoot
Enable live updates for all <OptimizedEntry /> components in your app:
<OptimizationRoot clientId="your-client-id" environment="your-environment" liveUpdates={true}>
{/* ... */}
</OptimizationRoot>3. Per-Component Override
Enable or disable live updates for specific components:
// This component will always react to changes immediately
<OptimizedEntry entry={dashboardEntry} liveUpdates={true}>
{(resolvedEntry) => <Dashboard data={resolvedEntry.fields} />}
</OptimizedEntry>
// This component locks to first variant, even if global liveUpdates is true
<OptimizedEntry entry={heroEntry} liveUpdates={false}>
{(resolvedEntry) => <Hero data={resolvedEntry.fields} />}
</OptimizedEntry>Priority Order
The live updates setting is determined for a particular <OptimizedEntry/> component in this order
(highest to lowest priority):
- Preview panel open - Always enables live updates (cannot be overridden)
- Component
liveUpdatesprop - Per-component override OptimizationRootliveUpdatesprop - Global setting- Default - Lock to first variant (
false)
| Preview Panel | Global Setting | Component Prop | Result |
| ------------- | -------------- | -------------- | ---------------- |
| Open | any | any | Live updates ON |
| Closed | true | undefined | Live updates ON |
| Closed | false | true | Live updates ON |
| Closed | true | false | Live updates OFF |
| Closed | false | undefined | Live updates OFF |
useLiveUpdates
The useLiveUpdates hook provides read access to the live updates state from the nearest
LiveUpdatesProvider. Use it to check the current global live updates setting or preview panel
visibility:
import { useLiveUpdates } from '@contentful/optimization-react-native'
function MyComponent() {
const liveUpdates = useLiveUpdates()
const isLive = liveUpdates?.globalLiveUpdates ?? false
return <Text>{isLive ? 'Live' : 'Locked'}</Text>
}React Native-Specific Defaults
The SDK automatically configures:
- Channel:
'mobile' - Library:
'@contentful/optimization-react-native' - Storage: AsyncStorage for persisting changes, consent, profile, and selected optimizations
- Event Builders: Mobile-optimized locale, page properties, and user agent detection
Persistence Behavior
AsyncStorage persistence is best-effort. If AsyncStorage write/remove calls fail, the SDK keeps running with in-memory state and retries persistence on future writes.
Structured cached values (changes, profile, selectedOptimizations) are schema-validated on
load and access. Malformed JSON or schema-invalid values are automatically removed from in-memory
cache and AsyncStorage.
Offline Support
The SDK automatically detects network connectivity changes and handles events appropriately when the device goes offline. To enable this feature, install the optional peer dependency:
pnpm install @react-native-community/netinfoOnce installed, the SDK will:
- Queue events when the device is offline
- Automatically flush queued events when connectivity is restored
- Flush events when the app goes to background (to prevent data loss)
No additional configuration is required - the SDK handles everything automatically.
How It Works
The SDK uses @react-native-community/netinfo to monitor network state changes. It prioritizes
isInternetReachable (actual internet connectivity) over isConnected (network interface
availability) for accurate detection.
| Platform | Native API Used |
| -------- | --------------------- |
| iOS | NWPathMonitor |
| Android | ConnectivityManager |
If @react-native-community/netinfo is not installed, the SDK will log a warning and continue
without offline detection. Events will still work normally when online.
Polyfills
The SDK includes automatic polyfills for React Native to support modern JavaScript features:
- Iterator Helpers (ES2025): Polyfilled using
es-iterator-helpersto support methods like.toArray(),.filter(),.map()on iterators crypto.randomUUID(): Polyfilled usingreact-native-uuidto ensure the universal EventBuilder works seamlesslycrypto.getRandomValues(): Polyfilled usingreact-native-get-random-valuesfor secure random number generation
These polyfills are imported automatically when you use the SDK - no additional setup required by your app.
