Integrate the Optimization React Native SDK in a React Native app
Overview
Use this guide to add Contentful personalization to a React Native or Expo app using
@contentful/optimization-react-native. By the end of the quick start, one Contentful entry will
render the personalized variant for the current visitor — or the original entry when none applies —
and one screen event will report the visitor’s screen view to Contentful, which uses it to keep that
visitor’s personalization consistent.
New to personalization? Here is the whole idea in five points:
- In Contentful you author variants of an entry and attach them to an experience — a rule that decides which visitors see which variant.
- As the app runs, Contentful’s Experience API looks at who the visitor is and picks the variant for each experience. Swapping a fetched entry for its picked variant is called resolving the entry.
- The Experience API also returns a profile: the anonymous, per-visitor identity and state used to keep personalization consistent across requests or app launches.
- Your app hands a Contentful entry to the SDK at the point where that entry becomes output. The SDK gives back the selected variant, or the original entry when no variant applies—the baseline fallback. You can fetch the entry yourself or give the SDK your Contentful client and an entry ID; either way, the client stays yours.
- You render the returned entry with the same application components you already use.
The React Native SDK persists the profile across app launches when persistence consent allows it.
That is enough to start. The guide introduces policy and optional capabilities at the point you need them.
You will get there in two milestones:
- Milestone 1 — one entry resolving and one screen event (the quick start below). A single screen
mounts
OptimizedEntry, which renders the resolved variant or the baseline, and reports one screen event. This is complete and shippable on its own. - Milestone 2 — the opt-in layers (later). Consent handoff, interaction tracking, identity, live updates, the preview panel, offline delivery, and analytics forwarding, each introduced by the section that needs it.
This guide uses @contentful/optimization-react-native. You mount one OptimizationRoot around your
app; it creates the SDK instance, restores state from AsyncStorage, and provides it to the hooks and
components below it. Your app still owns its Contentful Delivery API client, locale policy, consent
policy, identity policy, navigation, and final rendering.
Quick start
Most React Native + Contentful apps share one shape: a screen fetches or receives a Contentful
entry, and somewhere in that screen the entry becomes a rendered component. This quick start assumes
that shape and proves the smallest result: one screen renders a resolved entry — variant or
baseline — and reports one screen event. It mounts one OptimizationRoot, hands the SDK your
Contentful client so it can fetch the entry by ID, and tracks the screen with useScreenTracking.
This quick start assumes your application policy permits Optimization to start with accepted consent
and renders no end-user consent UI, so it seeds defaults={{ consent: true }} — the shorthand that
accepts both consent axes at once. If personalization must wait for a consent decision, keep this
structure and add the Consent and privacy-policy handoff step
before you ship, which explains the two axes and the object form that sets them separately.
-
Install the React Native SDK, its required AsyncStorage peer dependency, and a Contentful delivery client if your app does not already have one.
Copy this:
AsyncStorage ships native code, so complete your platform’s native install step before launching: run
pod installinios/for a bare React Native app, ornpx expo prebuild(a custom dev build) for Expo, then rebuild the app. -
Mount
OptimizationRootwith your app-owned Contentful client, emit one screen event for profile context, and render one single-locale Contentful entry by ID throughOptimizedEntry.Adapt this to your use case:
The
AppandHomeScreenscaffolding above is illustrative context to match against your own app, not a file to paste over yours. Wrap your existing app root inOptimizationRoot, adduseScreenTrackingto a screen you already render, and wrap one entry you already render inOptimizedEntry— keep the rest of your components as they are. -
Verify the first run. Launch the app; the screen displays a resolved entry ID for either the selected variant or the baseline. Because
logLevel="debug"is set above, the SDK logs its activity to the console, so you can confirm the screen event fired by watching your Metro or device logs on mount for thescreenevent the SDK sends (the Screen and navigation tracking and Analytics forwarding sections addstates.eventStreamfor a programmatic check). To see personalization rather than the baseline, author (in Contentful) a variant ofhero-entry-idattached to an experience that targets all visitors — every visitor matches it automatically, so the resolved ID changes to the variant. Without an authored variant every launch shows the baseline entry, which is expected, not a failure.
Before you start
The sections below walk the integration in order. First, gather the few things you can only get from outside this guide:
- A React Native or Expo app with React and React Native installed, its own Contentful fetching
already working, and the ability to run a native build step (
pod installfor bare React Native,npx expo prebuildfor Expo) — the SDK’s required AsyncStorage peer dependency ships native code. - Contentful delivery credentials — space ID, delivery token, and environment — read from your app’s runtime configuration.
- At least one entry with a variant attached to an experience, authored in Contentful. Without an authored variant, the integration can still run correctly while returning the baseline, so you cannot yet distinguish working personalization from a content-authoring gap. For the first personalized-content test, target all visitors so the test request or visitor matches automatically.
- Your Optimization project values — client ID and environment, from your Optimization project
settings. The Experience API (which picks variants) and the Insights API (which receives event and
interaction delivery) each have a base URL that defaults correctly; you only set them for mocks or
non-default hosts (see Install and initialize
OptimizationRoot).
You do not need a setup inventory up front. Everything else — consent, entry resolution, screen tracking, interaction tracking, identity, live updates, preview, offline delivery — is introduced by the section that needs it.
Read the SDK and Contentful config from your app’s runtime configuration. This guide’s examples use inline placeholder strings for clarity; the reference implementation reads PUBLIC_... environment variables through @env because it runs against shared mock defaults. Use whatever environment variable convention your React Native tooling already uses and keep it consistent.
Core integration
Install and initialize OptimizationRoot
Integration category: Required for first integration
You mounted OptimizationRoot in the quick start; this section covers its full configuration
surface — onStatesReady, api host overrides, logLevel, and reading the SDK instance with
useOptimization(). OptimizationRoot is the normal React Native entry point: it creates the SDK
instance, waits for AsyncStorage-backed state setup, runs onStatesReady when provided, then renders
provider children. It also composes live-update and interaction-tracking context for descendant
components.
- Mount one
OptimizationRootaround all components that call React Native SDK hooks. - Pass
clientIdfrom runtime configuration. Passenvironmentonly when you do not use the default Contentful environment; when you omit it the SDK usesmain. - Pass
localewhen Experience API responses and event context must use the same app locale as your Contentful entry fetches. - Pass
apiendpoint overrides only for staging, mocks, or non-default production hosts. Both base URLs default correctly otherwise, so most apps omitapientirely. - Provide
onStatesReadywhen app-level code must subscribe to SDK state before child effects run (see Analytics forwarding).
Adapt this to your use case:
Use useOptimization() under the provider when a component needs the SDK instance. The hook throws
outside OptimizationRoot or OptimizationProvider, and the provider-owned path withholds children
until the SDK is ready.
Consent and privacy-policy handoff
Integration category: Common but policy-dependent
Consent policy belongs to your application. Consent has two independent axes: events (may the SDK
personalize and send events) and persistence (may the SDK store profile continuity in
AsyncStorage). The shorthand consent: true sets both to true; the object form
{ events, persistence } sets them separately. The SDK stores event consent, stores separate durable
profile-continuity persistence consent, and blocks non-allowed event types until event consent is
accepted.
- If application policy permits Optimization by default and you do not render a user consent UI, seed accepted consent during SDK initialization.
- If consent depends on user choice, leave
defaults.consentunset and calloptimization.consent(true | false)from the application-owned banner, CMP callback, or settings flow. - Use object-form consent when events are permitted but profile continuity must stay session-only.
- Configure
allowedEventTypesonly after privacy review approves which events can emit before consent. - Subscribe to
states.blockedEventStreamduring development when you need to verify blocked calls.
Adapt this to your use case:
Adapt this to your use case:
By default, React Native permits identify and screen before event consent is accepted. Entry
views, entry taps, and any other event type are blocked until consent is accepted or that type is
allow-listed. Boolean consent calls control both event emission and durable profile continuity. Use
optimization.consent({ events: true, persistence: false }) when events can emit but the stored
profile-continuity state must stay session-only. That state is the anonymous identity, the profile,
the selected optimizations (which variant the Experience API picked for each experience), and the
changes (the profile-backed values the Experience API returns for feature flags — named on/off or
valued settings — and merge tags — profile-driven substitutions in Rich Text; both are covered in
Merge tags and Custom Flags) — all of which otherwise persist in
AsyncStorage across launches.
For cross-SDK policy details, see Consent management in the Optimization SDK Suite.
Contentful entry fetching and locale shape
Integration category: Required for first integration
Your app owns the Contentful client and locale policy. The React Native SDK can call that client
when contentful: { client } is configured, or your app can fetch a manual baselineEntry. Both
paths must produce a standard single-locale Contentful CDA entry payload where optimized fields are
direct values, not locale-keyed maps.
- Choose the application Contentful locale in your app configuration, i18n layer, or navigation layer.
- Configure
contentful.defaultQueryon the SDK, pass per-entryentryQuery, or pass the locale to manual Contentful CDA requests. - Request enough link depth for the SDK to resolve variants.
nt_experiences(plural) is a fixed Optimization link field the SDK adds to an optimized entry; it links that entry’s experiences, and each experience links its variant entries. Do not confuse it withnt_experience(singular), the content type of the experience entries it links to. Both are SDK-owned content-model names you do not choose. Your fetch mustincludedeeply enough to pull those linked entries back in one payload.include: 10is the repository reference implementation’s pattern. - Pass the same locale to SDK
localewhen Experience API responses and event context must use the same language. - Do not pass
contentful.jswithAllLocalesresults or raw CDAlocale=*responses toOptimizedEntry,useOptimizedEntry, oruseEntryResolver. - Pass
prefetchManagedEntriestoOptimizationRootorOptimizationProviderwhen a screen knows the managed entries it will render soon and you want the SDK to warm its managed-entry cache after readiness.
Adapt this to your use case:
Manual fetching remains supported when your app needs request ownership around one entry:
Adapt this to your use case:
Changing the provider locale prop after initialization calls sdk.setLocale(nextLocale). After
the locale update, Experience API requests and event context use the new locale, but the SDK does
not refetch Contentful entries or refresh profile state. Refetch entries and run your normal
screen(), identify(), or profile refresh path when localized data must update.
prefetchManagedEntries accepts entry ID strings or { entryId, entryQuery } descriptors. The
provider keeps rendering children while the cache warms. React Native does not expose
prefetchedManagedEntries because it has no server-to-client hydration handoff path.
For the entry contract, see Entry optimization and variant resolution. For the broader locale model, see Locale handling in the Optimization SDK Suite.
Entry resolution and fallback rendering
Integration category: Required for first integration
OptimizedEntry resolves the selected Contentful variant for the current profile and passes
non-optimized entries through unchanged. Invalid, incomplete, or unmatched optimization data falls
back to the baseline entry instead of throwing.
The entry source is a discriminated union: pass either entryId (managed fetch) or baselineEntry
(manual fetch), never both.
- Pass
entryIdto let the SDK fetch the baseline entry through the configured Contentful client (managed fetching, from the Contentful entry fetching and locale shape section). - Pass
baselineEntrywhen your application already fetched the entry and must keep manual request ownership. - Use
loadingFallback,errorFallback, andonEntryErrorwhen the managed fetch needs visible loading or error handling. - Use a render prop — a function passed as the child, which receives the resolved entry and returns
your UI — when the child needs the resolved baseline or variant entry. This is the
{(resolvedEntry) => ...}form the quick start used. - Use static children only when you need entry tracking but not variant data in the child.
- Use
useOptimizedEntry()for the same managed or manual source model without the wrapper component. - Use
useEntryResolver()when a component needs manual-only resolution helpers.
Adapt this to your use case:
When you use the useOptimizedEntry hook directly, isPresentationReady is true once the entry
has been fetched (for a managed entryId) and resolved, so the returned entry is safe to render;
gate your render on it as the example does. OptimizedEntry handles this for you and only calls the
render prop when the entry is ready.
The resolved entry has the same field shape as the baseline entry, handed to the render prop as a
base contentful Entry. Cast it to your content-type interface in the child
(resolvedEntry as HeroFields) when your renderer needs the narrower type; the plain direct cast
works for common narrowed types. Downstream renderers can render the fields without branching on
whether the SDK returned a variant.
There are two distinct outcomes, and they use different props. The baseline fallback is a
resolution outcome on an entry the SDK already has: on denied consent, no matching variant,
unresolved links, or an all-locale payload, the render prop receives the baseline (original) entry
and the UI does not break. A managed-fetch failure is different: when entryId is used and the
SDK’s fetch rejects, there is no entry to resolve, so onEntryError fires once and OptimizedEntry
renders errorFallback instead of the render prop. While a managed entryId fetch is unresolved,
OptimizedEntry shows loadingFallback until the fetch settles; there is no time limit on that
loading window, so provide a loadingFallback for any entry the reader waits on.
Screen and navigation tracking
Integration category: Required for first integration
Screen events update the mobile profile and route-aware event context. The SDK gives you a React Navigation adapter and two hook paths.
- Use
OptimizationNavigationContainerwhen your app uses React Navigation and you want automatic events for active route changes. - Use
useScreenTracking()inside a screen when the app does not use React Navigation or when the event must wait for screen data. - Use
useScreenTrackingCallback()for imperative tracking when the screen name comes from a deep link, navigation state transform, or another runtime value. - Avoid tracking the same route with both the navigation adapter and a screen hook.
- Set
includeParamsonOptimizationNavigationContaineronly when route params are approved for event payloads. Params are JSON-validated before they are attached.
Adapt this to your use case:
The automatic navigation path uses trackCurrentScreen() for current-screen deduplication. Direct
manual screen() calls are still direct event emits.
To verify screen tracking, open one tracked screen and confirm one accepted screen event through
SDK logs, states.eventStream, or your approved event inspection path.
Entry interaction tracking
Integration category: Common but policy-dependent
Entry interaction tracking records views and taps for Contentful entries. View and tap tracking are
enabled by default on OptimizedEntry.
- Decide whether entry view and tap events are allowed by your application’s Analytics and privacy policy.
- Set root defaults with
trackEntryInteractiononly when most entries need to opt out of a default interaction. - Override an individual entry with
trackViews,trackTaps, oronTap. - Wrap scrollable screens with
OptimizationScrollProviderso view tracking uses the actual scroll position. - Tune
minVisibleRatio,dwellTimeMs, andviewDurationUpdateIntervalMsonly when product analytics requirements differ from the defaults.
Adapt this to your use case:
The default view threshold is 80% visibility (minVisibleRatio 0.8) for 2000 ms (dwellTimeMs).
After the first view event, periodic duration updates emit every 5000 ms
(viewDurationUpdateIntervalMs) while the entry remains visible. Without
OptimizationScrollProvider, the SDK assumes scrollY is 0 and uses the screen height as the
viewport, which is appropriate only for non-scrollable or already-visible layouts.
React Native tracks two interactions: entry views and entry taps (there is no hover). A touch counts
as a tap only when the finger moves less than 10 points between touch start and end, so a scroll
gesture that starts on an entry does not register as a tap. On the wire, a tap is delivered as a
component_click event.
For event timing, scroll context, tap distance, and backgrounding mechanics, see React Native SDK interaction tracking mechanics.
Identity, profile continuity, and reset
Integration category: Common but policy-dependent
Identity policy belongs to your application. The SDK can send known-user traits, maintain profile-continuity state, and reset its in-memory profile state, but it does not decide when a user becomes known or how account data is governed.
- Call
identify({ userId, traits })after authentication or account state proves the user’s identity. - Keep traits limited to values approved for Optimization profile use.
- Call
reset()on sign-out, account switch, or privacy reset flows that must clear SDK profile state. - Use object-form consent when profile continuity persistence must differ from event consent.
- Implement any web, server, or account continuity handoff in application code. React Native uses AsyncStorage and does not provide a built-in browser-cookie handoff.
Adapt this to your use case:
AsyncStorage persists consent state and, when persistence consent permits it, profile, selected optimizations, changes, and anonymous identity across app launches. It does not persist SDK event queues. Live SDK state after startup comes from in-memory SDK state, not repeated AsyncStorage reads.
For advanced anonymous ID ownership, pass getAnonymousId to OptimizationRoot or
ContentfulOptimization.create(...) only when your app owns an approved anonymous ID that
Optimization can use. Otherwise, omit it and rely on the React Native SDK’s AsyncStorage-backed
anonymous ID default.
Optional integrations
Merge tags and Custom Flags
Integration category: Optional
Use merge tags and Custom Flags when your app renders profile-backed Rich Text values or feature
values returned in the Experience API changes data. Entry replacement and flag rendering are
separate decisions: OptimizedEntry chooses an entry variant, while flags and merge tags read
profile-backed values from SDK state.
- Resolve merge tags inside your app-owned Rich Text renderer with the SDK instance returned by
useOptimization(). - Subscribe to
sdk.states.flag(name)when a component needs to rerender as a flag value changes. - Treat flag values and merge-tag values as profile-dependent runtime data, not as static Contentful content.
Adapt this to your use case:
For the deeper data model, see Entry optimization and variant resolution.
Live updates
Integration category: Optional
By default, OptimizedEntry locks to the first selected variant it receives for that component
lifetime. This prevents visible content changes when profile state changes while the user is viewing
the entry.
- Set
liveUpdatesonOptimizationRootwhen all optimized entries in the app can react to profile and variant changes. - Set
liveUpdateson an individualOptimizedEntrywhen only one section can update in place. - Use
useLiveUpdates()only for UI that needs to display or react to the live-update state. - Remember that the preview panel forces live updates while it is open.
Adapt this to your use case:
Live-update resolution order is preview panel open, component liveUpdates prop, OptimizationRoot
liveUpdates prop, then the default locked behavior.
Preview panel
Integration category: Optional
The preview panel is an in-app authoring and debugging surface. It fetches your audience and
experience definitions (nt_audience and nt_experience, the fixed Optimization content types, not
names you choose) through the Contentful client you provide, lets users override audiences and
variants locally, and forces live updates while the panel is open.
- Install the preview peer dependencies before importing the preview subpath.
- Mount
PreviewPanelOverlayunderOptimizationRoot. - Pass a Contentful client that can fetch the space and environment containing Optimization audience and experience entries.
- Gate the panel behind
__DEV__or an equivalent internal build flag. - Provide
onRefreshwhen the panel must trigger a fresh Experience API request after overrides.
Copy this:
Adapt this to your use case:
PreviewPanelOverlay must be rendered inside OptimizationRoot, or inside both
OptimizationProvider and LiveUpdatesProvider. Expo apps that use preview native modules need a
custom dev build, such as expo run:ios or expo run:android; Expo Go is not enough.
Offline event delivery
Integration category: Optional
When @react-native-community/netinfo is installed, the SDK listens for connectivity changes.
NetInfo gates flushing while the app is offline and resumes flushing when the device is reachable.
Offline replay is in memory while the JavaScript process remains alive; NetInfo does not create a
durable event outbox. When NetInfo is absent, the SDK logs a warning and runs without offline
detection.
- Install NetInfo when the app must replay in-memory SDK events after offline periods.
- Keep queue sizing and drop policy aligned with product Analytics requirements.
- Verify that app backgrounding flushes queues and drains pending AsyncStorage writes on your supported platforms. AsyncStorage persistence covers consent and profile continuity, not event queues.
Copy this:
The SDK also listens for React Native AppState transitions to background or inactive and calls
flush() before the operating system can suspend the process.
Analytics forwarding
Integration category: Optional
Use Analytics forwarding when your mobile app already sends events to a customer-data platform, product analytics destination, or vendor SDK. The Optimization SDK still sends its own events to Contentful. Your application decides which approved Contentful context can also be forwarded.
- Attach app-level subscriptions with
onStatesReadyso subscribers register before child effects can emit screen, entry, or blocked-event updates. - Read
states.eventStreamfor SDK events that were accepted. - Read
states.blockedEventStreamto verify consent andallowedEventTypesblocks during development. - Dedupe forwarded events by
messageIdor by destination-specific semantic keys when one user action produces multiple event deliveries. - Store forwarded message IDs in module or app state so remounts do not forward the same event
again. If the destination must receive only future SDK events, read the current
messageIdbefore subscribing and skip that event. - Apply the same consent, privacy, and data-minimization policy to forwarded payloads that you apply to first-party SDK events.
Adapt this to your use case:
Use Forwarding Optimization SDK context to analytics and tag-management tools for destination mapping, consent, identity, dedupe, and governance guidance.
Advanced integrations
Explicit SDK instance ownership
Integration category: Advanced or production-only
Use explicit instance ownership only when a framework adapter, test harness, or application service
must create the SDK before React renders. OptimizationRoot is the preferred path for normal React
Native apps.
- Create the SDK with
await ContentfulOptimization.create(config).createisasyncand returns aPromise<ContentfulOptimization>because it reads AsyncStorage before constructing. - Pass the resolved instance to
OptimizationProvider sdk={sdk}. - Wrap the tree in the exported
LiveUpdatesProvideronly when preview tooling or global live-update state must work withoutOptimizationRoot. - Call
destroy()from the owner during teardown, once the instance is done.OptimizationProviderdoes not destroy an injected SDK;destroy()clears the singleton so a new instance can be created. - Do not create a second active React Native SDK instance without destroying the first one:
createthrowsContentfulOptimization React Native SDK is already initialized. Reuse the existing instance.while a live instance exists. - Use per-entry
trackViewsandtrackTapsfor interaction policy in injected-provider flows;trackEntryInteractionis anOptimizationRootconvenience.
Follow this pattern:
Strict event admission and queue controls
Integration category: Advanced or production-only
Use strict controls when privacy review, regulated deployments, or constrained mobile networks need behavior beyond the default React Native settings.
- Set
allowedEventTypes={[]}when no event can emit before explicit event consent. - Use
onEventBlockedto surface consent or guard failures in diagnostics. - Configure
queuePolicy.offlineMaxEventsandqueuePolicy.onOfflineDropwhen the offline Experience buffer must have explicit bounds and observability. - Configure
queuePolicy.flushonly when flush behavior must differ from SDK defaults across shared Insights and Experience delivery. - Use
apiendpoint overrides andfetchOptionsfor staging, mocks, and request-level options such asrequestTimeoutandretries.
Adapt this to your use case:
Platform build boundaries
Integration category: Advanced or production-only
React Native platform setup determines which optional SDK behavior is available at runtime.
- Run native install steps for AsyncStorage and any optional native peer dependency you add.
- Use a custom Expo dev build for preview panel dependencies; Expo Go cannot load the required native modules.
- Keep preview UI out of production builds with build flags or internal distribution channels.
- When testing against a local mock server on the Android emulator, rewrite
localhostAPI hosts to the emulator host alias10.0.2.2in your own app configuration. The SDK has no localhost rewrite; this is app-config you own. The reference implementation does exactly this inenv.config.ts(localhost→10.0.2.2on Android; the iOS Simulator uses the host network directly). - For release builds, verify that the Contentful CDA host, Experience API host, and Insights API host point to the intended environment.
Production checks
Before release, verify these checks in the app build and environment that will ship:
- Credentials and runtime configuration - The app uses the intended Optimization
clientId, Optimization environment, API hosts, Contentful space, Contentful environment, CDA token, and app locale. Android emulator-only localhost rewrites are not present in production configuration. - Consent behavior - Default accepted consent is used only when policy permits it. User-choice
flows call
consent(true | false), object-form consent matches the persistence policy, and rejected consent blocks non-allowed event types. - Event delivery - Screen, identify, entry view, entry tap, Custom Flag, and forwarded Analytics events appear in the expected destinations. In-memory offline replay and background flushing are verified when NetInfo is installed.
- Content fallback behavior - Optimized entries are fetched with one CDA locale and enough link depth. Non-optimized, unmatched, or incomplete entries render baseline content instead of blank UI.
- Duplicate tracking prevention - One
OptimizationRootowns the app runtime, each route uses one screen-tracking path, and the app does not wrap the same rendered entry multiple times for one impression. Forwarded events are deduped before leaving the app. - Privacy and governance - Forwarded Analytics payloads are allow-listed, profile traits are approved, preview UI is absent from production builds, and persisted profile continuity matches consent records.
- Local validation path - Compare behavior against the React Native reference implementation and run the smallest meaningful app validation for the changed flow, such as typecheck, lint, or a targeted Detox file through the repository runner.
Troubleshooting
Reference implementations to compare against
- React Native reference implementation - The in-tree React Native app that demonstrates provider setup, consent bootstrap, CDA locale handling, optimized entry rendering, scroll provider usage, tap tracking, navigation tracking, live updates, preview panel behavior, offline behavior, and Detox E2E coverage.