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.
Install using an NPM-compatible package manager, pnpm for example:
pnpm install @contentful/optimization-react-native @react-native-async-storage/async-storage
For offline support (recommended), also install:
pnpm install @react-native-community/netinfo
Import the Optimization React Native SDK; both CJS and ESM module systems are supported, ESM preferred:
import { OptimizationReactNativeSdk } from '@contentful/optimization-react-native'
Configure and initialize the Optimization React Native SDK:
const optimization = await OptimizationReactNativeSdk.create({
clientId: 'your-client-id',
environment: 'main',
})
| Option | Required? | Default | Description |
|---|---|---|---|
allowedEventTypes |
No | ['identify', 'screen'] |
Allow-listed event types permitted when consent is not set |
analytics |
No | See "Analytics Options" | Configuration specific to the Analytics/Insights API |
clientId |
Yes | N/A | The Optimization API key |
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 |
personalization |
No | See "Personalization Options" | Configuration specific to the Personalization/Experience API |
preventedComponentEvents |
No | undefined |
Initial duplication prevention configuration for component events |
Configuration method signatures:
getAnonymousId: () => string | undefined| Option | Required? | Default | Description |
|---|---|---|---|
baseUrl |
No | 'https://ingest.insights.ninetailed.co/' |
Base URL for the Insights API |
beaconHandler |
No | undefined |
Handler used to enqueue events via the Beacon API or a similar mechanism |
Configuration method signatures:
beaconHandler: (url: string | URL, data: BatchInsightsEventArray) => booleanEvent builder options should only be supplied when building an SDK on top of the Optimization React Native SDK or any of its descendent 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: 'Optimization React Native SDK', version: '<pkg version>' } |
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:
webmobileserverConfiguration method signatures:
getLocale: () => string | undefined
getPageProperties:
() => {
path: string,
query: Record<string, string>,
referrer: string,
search: string,
title?: string,
url: string
}
getUserAgent: () => string | undefined
Fetch options allow for configuration of a Fetch API-compatible fetch method and the retry/timeout
logic integrated into the Optimization API Client. 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>onFailedAttempt and onRequestTimeout: (options: FetchMethodCallbackOptions) => void| Option | Required? | Default | Description |
|---|---|---|---|
baseUrl |
No | 'https://experience.ninetailed.co/' |
Base URL for the Experience API |
enabledFeatures |
No | ['ip-enrichment', 'location'] |
Enabled features which the API may use for each request |
ip |
No | undefined |
IP address to override the API behavior for IP analysis |
locale |
No | 'en-US' (in API) |
Locale used to translate location.city and location.country |
plainText |
No | false |
Sends performance-critical endpoints in plain text |
preflight |
No | false |
Instructs the API to aggregate a new profile state but not store it |
Call OptimizationReactNativeSdk.create(...) once per app runtime and share the returned
instance. In tests or hot-reload workflows, call destroy() before creating a replacement
instance.
Important: When we refer to "component tracking," we're talking about tracking Contentful entry components (content entries in your CMS), NOT React Native UI components. The term "component" comes from Contentful's terminology for personalized content entries.
The SDK provides two semantic components for tracking different types of Contentful entries:
<Personalization /> - For Personalized EntriesUse this component to track Contentful entries that can be personalized (have nt_experiences
field). It automatically:
<Analytics /> - For Non-Personalized EntriesUse this component to track standard Contentful entries you want analytics on (articles, etc.). It:
<Personalization />Both components track when an entry:
threshold prop)viewTimeMs prop)The tracking components work in two modes:
When used inside a <OptimizationScrollProvider>, tracking uses the actual scroll position and
viewport dimensions:
<OptimizationScrollProvider>
<Personalization baselineEntry={entry}>
{(resolvedEntry) => <HeroComponent data={resolvedEntry} />}
</Personalization>
<Analytics entry={productEntry}>
<ProductCard data={productEntry.fields} />
</Analytics>
</OptimizationScrollProvider>
Benefits:
When used without <OptimizationScrollProvider>, tracking uses screen dimensions instead:
<Personalization baselineEntry={entry}>
{(resolvedEntry) => <FullScreenHero data={resolvedEntry} />}
</Personalization>
<Analytics entry={bannerEntry}>
<Banner data={bannerEntry.fields} />
</Analytics>
Note: In this mode, scrollY is always 0 and viewport height equals the screen height. This
is ideal for:
Both components support customizable visibility and time thresholds:
<Personalization
baselineEntry={entry}
viewTimeMs={3000} // Track after 3 seconds of visibility
threshold={0.9} // Require 90% visibility
>
{(resolvedEntry) => <YourComponent data={resolvedEntry.fields} />}
</Personalization>
<Analytics
entry={entry}
viewTimeMs={1500} // Track after 1.5 seconds
threshold={0.5} // Require 50% visibility
>
<YourComponent />
</Analytics>
Key Features:
OptimizationScrollProvider (automatically adapts)<Personalization /> uses render prop pattern to provide resolved entry<Analytics /> uses standard children patternYou can also manually track events using the analytics API:
import { useOptimization } from '@contentful/optimization-react-native'
function MyComponent() {
const optimization = useOptimization()
const trackManually = async () => {
await optimization.trackComponentView({
componentId: 'my-component',
experienceId: 'exp-456',
variantIndex: 0,
})
}
return <Button onPress={trackManually} title="Track" />
}
OptimizationRoot is the recommended way to set up the SDK. It combines OptimizationProvider with
optional preview panel functionality:
import {
OptimizationReactNativeSdk,
OptimizationRoot,
OptimizationScrollProvider,
} from '@contentful/optimization-react-native'
import { createClient } from 'contentful'
const contentfulClient = createClient({
space: 'your-space-id',
accessToken: 'your-access-token',
})
const optimization = await OptimizationReactNativeSdk.create({
clientId: 'your-client-id',
environment: 'your-environment',
})
function App() {
return (
<OptimizationRoot
instance={optimization}
previewPanel={{
enabled: __DEV__, // Only show in development
contentfulClient: contentfulClient,
}}
>
<OptimizationScrollProvider>{/* Your app content */}</OptimizationScrollProvider>
</OptimizationRoot>
)
}
When previewPanel.enabled is true, a floating action button appears that opens the preview
panel. The panel allows developers to:
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
instance={optimization}
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>
By default, <Personalization /> 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 personalizations mid-session.
// User sees Variant A on initial load
<Personalization baselineEntry={heroEntry}>
{(resolvedEntry) => <Hero data={resolvedEntry.fields} />}
</Personalization>
// Even if the user later qualifies for Variant B (e.g., after identify()),
// they continue to see Variant A until the component unmounts
This provides a stable user experience where content doesn't unexpectedly change while the user is viewing it.
There are three ways to enable live updates (immediate reactions to personalization changes):
When the preview panel is open, all <Personalization /> components automatically enable live
updates. This allows developers to test different variants without refreshing the screen:
<OptimizationRoot instance={optimization} previewPanel={{ enabled: true, contentfulClient }}>
{/* All Personalization components will live-update when panel is open */}
</OptimizationRoot>
Enable live updates for all <Personalization /> components in your app:
<OptimizationRoot instance={optimization} liveUpdates={true}>
{/* ... */}
</OptimizationRoot>
Enable or disable live updates for specific components:
// This component will always react to changes immediately
<Personalization baselineEntry={dashboardEntry} liveUpdates={true}>
{(resolvedEntry) => <Dashboard data={resolvedEntry.fields} />}
</Personalization>
// This component locks to first variant, even if global liveUpdates is true
<Personalization baselineEntry={heroEntry} liveUpdates={false}>
{(resolvedEntry) => <Hero data={resolvedEntry.fields} />}
</Personalization>
The live updates setting is determined for a particular <Personalization/> component in this order
(highest to lowest priority):
liveUpdates prop - Per-component overrideOptimizationRoot liveUpdates prop - Global settingfalse)| 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 |
The SDK automatically configures:
'mobile''Optimization React Native SDK'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, personalizations) are schema-validated on load and
access. Malformed JSON or schema-invalid values are automatically removed from in-memory cache and
AsyncStorage.
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/netinfo
Once installed, the SDK will:
No additional configuration is required - the SDK handles everything automatically.
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.
The SDK includes automatic polyfills for React Native to support modern JavaScript features:
es-iterator-helpers to support methods like
.toArray(), .filter(), .map() on iteratorscrypto.randomUUID(): Polyfilled using react-native-uuid to ensure the universal
EventBuilder works seamlesslycrypto.getRandomValues(): Polyfilled using react-native-get-random-values for secure
random number generationThese polyfills are imported automatically when you use the SDK - no additional setup required by your app.
Contentful Optimization React Native SDK.
Remarks
Implements React Native-specific functionality on top of the Optimization Core Library. Provides components for personalization, analytics tracking, and a preview panel for debugging personalizations during development.