> This page is for Personalization.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful-staging.ferndocs.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful-staging.ferndocs.com/developers/docs/_mcp/server.

# Integrate the Optimization React Native SDK in a React Native app

> This guide helps you add personalization, analytics, screen tracking, and a preview panel to a React Native (or Expo) application.

## Overview

This guide can help you add personalization, analytics, screen tracking, and a preview panel to a React Native (or Expo) application using `@contentful/optimization-react-native`.

> **Info**
>
> The Optimization SDK is currently in beta. For more information, [reach out](https://www.contentful.com/contact/sales/) to your Sales representative.

## Scope and capabilities

The React Native SDK builds on the Optimization Core Library and adds React Native-specific providers, hooks, and components. It lets consumers:

* Initialize and own a mobile SDK instance through `OptimizationRoot` or explicit providers
* Persist consent, profile state, selected optimizations, and anonymous identity with AsyncStorage
* Personalize Contentful entries with `OptimizedEntry`
* Emit entry view and tap tracking from React Native components
* Emit screen events through React Navigation adapters or screen-level hooks
* Opt into live updates and attach the in-app preview panel for authoring workflows
* Queue events while offline when NetInfo is available

The React Native SDK does not replace your Contentful delivery client. Your application still fetches Contentful entries, decides how consent works, decides when a user becomes known, and controls where personalized content renders.

## The integration flow

Most React Native integrations follow this sequence:

1. Install the SDK and its required peer dependencies.
2. Wrap the app in `OptimizationRoot` with the minimum config (`clientId`).
3. Decide how consent behaves (default-on for trusted contexts, or gated by a UI prompt).
4. Wrap each personalizable Contentful entry in `<OptimizedEntry>`.
5. Enable view and/or tap tracking for the entries you care about.
6. Wrap scrollable screens in `<OptimizationScrollProvider>` so viewport tracking is accurate.
7. Add screen tracking either automatically with `OptimizationNavigationContainer` or per screen with `useScreenTracking`.

Optional additions include live updates when entries need to continuously react to optimization state changes, and the preview panel when the application needs authoring or preview overrides.

The React Native reference implementation in our [GitHub repository](https://github.com/contentful/optimization) shows those patterns in a working application:

* [`implementations/react-native-sdk`](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md)

## 1. Install and initialize with minimal configuration

### Install peer dependencies

```sh
pnpm add @contentful/optimization-react-native @react-native-async-storage/async-storage
```

For offline support (recommended), also install:

```sh
pnpm add @react-native-community/netinfo
```

The SDK uses AsyncStorage to persist consent, profile, and selected optimizations across app launches. `netinfo` is optional but lets the SDK queue events while the device is offline and flush them automatically when connectivity returns.

> **Info**
>
> The Optimization SDK depends on native modules (e.g. `@react-native-clipboard/clipboard` for the preview panel). Expo apps using Optimization need a custom dev build (`expo run:ios` / `expo run:android`) — Expo Go is not enough. The in-tree React Native reference implementation [README](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md) documents the monorepo setup and end-to-end commands.

### The minimum setup

Wrap your app's root in `<OptimizationRoot>`. This is the recommended entry point — it composes the `OptimizationProvider`, `LiveUpdatesProvider`, and `InteractionTrackingProvider` for you and manages the SDK instance lifecycle.

```tsx
import { OptimizationRoot } from '@contentful/optimization-react-native'

export default function App() {
  return (
    <OptimizationRoot clientId="your-client-id">
      <YourApp />
    </OptimizationRoot>
  )
}
```

That is the minimum viable setup. `clientId` is the only required prop. Everything else falls back to safe defaults (environment defaults to `'main'`, channel to `'mobile'`, etc.).

A fuller application usually adds environment-specific config, a defaults block, optional preview panel settings, and navigation integration:

```tsx
<OptimizationRoot
  clientId={OPTIMIZATION_CLIENT_ID}
  environment={OPTIMIZATION_ENVIRONMENT}
  logLevel={__DEV__ ? 'info' : 'warn'}
  defaults={{ consent: true }}
  previewPanel={{
    enabled: __DEV__,
    contentfulClient: client,
  }}
>
  <OptimizationNavigationContainer>
    {(navigationProps) => (
      <NavigationContainer {...navigationProps}>
        {/* ...stack/tab navigators... */}
      </NavigationContainer>
    )}
  </OptimizationNavigationContainer>
</OptimizationRoot>
```

Common props on `OptimizationRoot`:

| Prop                    | Type                         | Required | Default                        | Description                                                           |
| ----------------------- | ---------------------------- | -------- | ------------------------------ | --------------------------------------------------------------------- |
| `clientId`              | `string`                     | Yes      | N/A                            | Your Contentful Optimization client identifier                        |
| `environment`           | `string`                     | No       | `'main'`                       | Optimization environment to read from                                 |
| `defaults`              | `{ consent?: boolean, ... }` | No       | `undefined`                    | Initial values applied at startup (e.g. `consent: true`)              |
| `logLevel`              | `LogLevels`                  | No       | `'error'`                      | Minimum console log level                                             |
| `previewPanel`          | `PreviewPanelConfig`         | No       | `undefined`                    | Enables the in-app preview panel; see [Preview Panel](#preview-panel) |
| `liveUpdates`           | `boolean`                    | No       | `false`                        | Global live-updates default for `<OptimizedEntry />`                  |
| `trackEntryInteraction` | `{ views?, taps? }`          | No       | `{ views: true, taps: false }` | Default interaction tracking for `<OptimizedEntry />`                 |
| `onStatesReady`         | `(states) => cleanup`        | No       | `undefined`                    | Attach app-level state subscribers when SDK state is ready            |

The full configuration reference (API endpoints, fetch retries, queue policy, event-builder overrides) is documented in the [React Native SDK README](https://github.com/contentful/optimization/blob/main/packages/react-native-sdk/README.md#common-configuration).

### Access the SDK instance with hooks

Inside the provider tree, use `useOptimization()` to interact with the SDK directly:

```tsx
import { useOptimization } from '@contentful/optimization-react-native'

function MyComponent() {
  const optimization = useOptimization()

  const handlePress = async () => {
    await optimization.identify('user-123', { plan: 'pro' })
  }

  return <Button onPress={handlePress} title="Identify" />
}
```

`useOptimization()` throws if used outside an `OptimizationProvider` / `OptimizationRoot`, and is guaranteed to return a ready SDK instance. `OptimizationProvider` does not render its children until the SDK has finished initializing.

React Native provider-owned initialization is async: the provider renders no children while platform state setup is pending, runs any `onStatesReady` callback, and then renders children. This matches the React Web provider contract, but React Native cannot use the Web layout-effect timing because SDK creation depends on async storage and platform APIs.

### Subscribe to SDK state from the provider

Use `onStatesReady` when application code needs SDK state subscriptions that line up with provider initialization. The provider calls it after async SDK state setup completes and before child screen, navigation, or entry effects can emit events.

```tsx
<OptimizationRoot
  clientId="your-client-id"
  onStatesReady={(states) => {
    const subscriptions = [
      states.eventStream.subscribe((event) => {
        if (event) devToolsPanel.logEvent(event)
      }),
      states.blockedEventStream.subscribe((blocked) => {
        if (blocked) devToolsPanel.logBlockedEvent(blocked)
      }),
    ]

    return () => {
      subscriptions.forEach((subscription) => subscription.unsubscribe())
    }
  }}
>
  <OptimizationNavigationContainer>
    {(navigationProps) => (
      <NavigationContainer {...navigationProps}>{/* navigators */}</NavigationContainer>
    )}
  </OptimizationNavigationContainer>
</OptimizationRoot>
```

`onStatesReady` receives only the `states` surface. Use regular hooks and React effects for component-local state.

Use `OptimizationProvider` directly with a pre-built `sdk` only when an application or framework adapter owns initialization. Without `onStatesReady`, children render immediately because the SDK is already available. When `onStatesReady` is provided, the provider waits until those subscribers are attached before children mount and runs the returned cleanup on unmount. In both cases, it does not call `destroy()` on the injected SDK.

## 2. Handle consent

The SDK gates non-essential event types behind a consent state. By default, only `identify` and `screen` events are allowed before consent is explicitly set. All other event types (including entry view/tap tracking) are blocked until the user accepts or rejects consent.

You can change which event types are permitted before consent via the `allowedEventTypes` config
option.

### Defaulting consent to `true`

If your app already collects consent at install time (e.g. through a prior onboarding flow) or if you don't need a runtime consent prompt, set `defaults.consent: true` so events flow immediately:

```tsx
<OptimizationRoot clientId="your-client-id" defaults={{ consent: true }}>
  <YourApp />
</OptimizationRoot>
```

The default is applied once at startup. User input later takes precedence. The [React Native reference implementation](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md) shows an equivalent trusted-context shortcut by calling `sdk.consent(true)` after the provider initializes.

### Gating consent on a banner

For apps that need an explicit prompt, leave `defaults.consent` unset and call `consent()` from your banner UI:

```tsx
import { useOptimization } from '@contentful/optimization-react-native'
import { View, Text, Button } from 'react-native'

function ConsentBanner() {
  const optimization = useOptimization()

  return (
    <View>
      <Text>Allow personalized experiences and analytics?</Text>
      <Button title="Accept" onPress={() => optimization.consent(true)} />
      <Button title="Reject" onPress={() => optimization.consent(false)} />
    </View>
  )
}
```

When consent is accepted (`true`), all event types are permitted. When consent is rejected (`false`), non-allowed event types are blocked and `<OptimizedEntry />` view/tap tracking will be silently dropped at the SDK boundary. Consent state persists across app launches via AsyncStorage.

### Reading and reacting to consent state

Subscribe to the SDK's consent observable when you need to render UI conditionally:

```tsx
import { useOptimization } from '@contentful/optimization-react-native'
import { useEffect, useState } from 'react'
import { Text } from 'react-native'

function ConsentStatus() {
  const optimization = useOptimization()
  const [consent, setConsent] = useState<boolean | undefined>(undefined)

  useEffect(() => {
    const sub = optimization.states.consent.subscribe(setConsent)
    return () => sub.unsubscribe()
  }, [optimization])

  return <Text>Consent: {String(consent)}</Text>
}
```

A common pattern is to gate personalized content rendering on consent and fall back to the baseline entry while consent is missing or rejected.

### Revoking consent

To revoke consent after it was previously accepted, just call `consent(false)`:

```tsx
optimization.consent(false)
```

## 3. Personalize entries with OptimizedEntry

`<OptimizedEntry />` is the unified component for resolving optimized variants and tracking interactions on Contentful entries. It:

* Detects whether the entry has `nt_experiences` (i.e. is optimized) and resolves the correct variant for the current user profile.
* Passes non-optimized entries through unchanged (so you can blanket-wrap a list and only the optimized entries actually personalize).
* Emits view tracking when the entry satisfies the visibility and dwell-time requirements.
* Emits tap tracking when enabled.

### Fetch the entry with `include: 10`

For variant data to resolve, the entry must be fetched with linked optimization references included. Use `include: 10` on Contentful's Delivery API call:

```tsx
const cta = await contentfulClient.getEntry(CTA_ENTRY_ID, { include: 10 })
```

The [React Native reference implementation](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md) centralizes this Contentful fetching pattern in its application helper layer.

### Render the variant with a render prop

Pass the baseline entry to `<OptimizedEntry>` and render with a render prop that receives the resolved entry:

```tsx
import { OptimizedEntry } from '@contentful/optimization-react-native'

function HeroSection({ baselineEntry }) {
  return (
    <OptimizedEntry baselineEntry={baselineEntry}>
      {(resolvedEntry) => <CTAHeader entry={resolvedEntry} />}
    </OptimizedEntry>
  )
}
```

`resolvedEntry` is either the resolved variant (when the SDK has selected one for the current profile) or the baseline entry (when no variant qualifies). Either way, `resolvedEntry.fields` has the same shape as the baseline — so the renderer downstream doesn't need to know whether it's seeing a variant or not.

Use `useEntryResolver()` when a component needs the same resolution behavior without the `OptimizedEntry` wrapper:

```tsx
import { useEntryResolver } from '@contentful/optimization-react-native'

function HeroData({ baselineEntry }) {
  const { resolveEntry } = useEntryResolver()
  const resolvedEntry = resolveEntry(baselineEntry)

  return <CTAHeader entry={resolvedEntry} />
}
```

### Pass-through for non-optimized entries

When you only want to track an entry (no variant resolution), pass static children instead of a render prop:

```tsx
<OptimizedEntry baselineEntry={blogPost}>
  <BlogPostCard post={blogPost} onPress={...} />
</OptimizedEntry>
```

This is the same tracking pattern used by the [React Native reference implementation](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md): entries are wrapped so the SDK can track views/taps, while non-optimized content passes through unchanged.

## 4. Interaction tracking with OptimizedEntry

`<OptimizedEntry />` tracks two interactions: **views** (the entry was at least N% visible for at least M ms) and **taps** (the user tapped the entry). View tracking is enabled by default. Tap tracking is opt-in.

For the deeper event timing, visibility ratio, consent-gating, and viewport-state details, see [React Native SDK Interaction Tracking Mechanics](/personalization/optimization-sdk/react-native-sdk-interaction-tracking-mechanics/).

### Global defaults via OptimizationRoot

Set a global default for all `<OptimizedEntry />` components via `trackEntryInteraction` on the root:

```tsx
<OptimizationRoot clientId="your-client-id" trackEntryInteraction={{ views: true, taps: true }}>
  <YourApp />
</OptimizationRoot>
```

The default is `{ views: true, taps: false }`.

### Per-component overrides

Override the global setting on individual entries with `trackViews` and `trackTaps`.

Track taps on a CTA, regardless of the global setting:

```tsx
<OptimizedEntry baselineEntry={cta} trackTaps>
  {(resolved) => <CTAHeader entry={resolved} />}
</OptimizedEntry>
```

Disable view tracking for a high-frequency entry:

```tsx
<OptimizedEntry baselineEntry={feedItem} trackViews={false}>
  <FeedItemCard item={feedItem} />
</OptimizedEntry>
```

You can also pass `onTap` to receive the resolved entry after a tap is tracked. Providing `onTap` implicitly enables tap tracking unless `trackTaps={false}` is explicit:

```tsx
<OptimizedEntry
  baselineEntry={cta}
  onTap={(resolved) => navigation.navigate('CTA', { id: resolved.sys.id })}
>
  {(resolved) => <CTAHeader entry={resolved} />}
</OptimizedEntry>
```

### Custom visibility and dwell timing

By default, view tracking fires when the entry is **80% visible for 2000 ms**. Customize per-entry:

```tsx
<OptimizedEntry
  baselineEntry={hero}
  minVisibleRatio={0.5} // 50% visible
  dwellTimeMs={1000} // for 1 second
>
  {(resolved) => <Hero entry={resolved} />}
</OptimizedEntry>
```

After the initial view event, the SDK emits periodic view-duration update events every 5000 ms by default. Configure with `viewDurationUpdateIntervalMs`.

### Use OptimizationScrollProvider for scrollable screens

Inside a scrolling container, viewport-based view tracking needs to know the actual scroll position. Wrap the scrollable screen in `<OptimizationScrollProvider>`:

```tsx
import { OptimizationScrollProvider, OptimizedEntry } from '@contentful/optimization-react-native'

function BlogPostDetailScreen({ post }) {
  return (
    <OptimizationScrollProvider>
      <OptimizedEntry baselineEntry={post}>
        <ArticleBody post={post} />
      </OptimizedEntry>
    </OptimizationScrollProvider>
  )
}
```

The [React Native reference implementation](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md) wraps its entry list in `OptimizationScrollProvider` before rendering optimized entries.

Without `OptimizationScrollProvider`, the SDK assumes scroll position is always `0` and the viewport equals the screen. That's fine for a single full-screen component, but for content that appears below the fold, wrap the screen so tracking fires when the user scrolls the entry into view.

## 5. Screen tracking

Screen tracking emits a `screen` event each time the user navigates to a new screen. The SDK uses these events to update profile attribution and route-aware properties.

### Automatic tracking with OptimizationNavigationContainer

If you use React Navigation, the easiest setup is `<OptimizationNavigationContainer />`, which wraps `<NavigationContainer />` and emits a `screen` event on every active route change:

```tsx
import { NavigationContainer } from '@react-navigation/native'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import {
  OptimizationRoot,
  OptimizationNavigationContainer,
} from '@contentful/optimization-react-native'

const Stack = createNativeStackNavigator()

export default function App() {
  return (
    <OptimizationRoot clientId="your-client-id">
      <OptimizationNavigationContainer>
        {(navigationProps) => (
          <NavigationContainer {...navigationProps}>
            <Stack.Navigator>
              <Stack.Screen name="Home" component={HomeScreen} />
              <Stack.Screen name="BlogPostDetail" component={BlogPostDetailScreen} />
            </Stack.Navigator>
          </NavigationContainer>
        )}
      </OptimizationNavigationContainer>
    </OptimizationRoot>
  )
}
```

The [React Native reference implementation](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md) exercises this adapter in its navigation test flow. The render-prop pattern means the wrapper does not depend on `@react-navigation/native` directly — navigation props are passed through to your real `NavigationContainer`.

Available props:

| Prop            | Required | Default | Description                                                    |
| --------------- | -------- | ------- | -------------------------------------------------------------- |
| `children`      | Yes      | N/A     | Render prop receiving `ref`, `onReady`, and `onStateChange`    |
| `onStateChange` | No       | —       | Called after screen tracking fires on navigation state changes |
| `onReady`       | No       | —       | Called after the initial screen tracking on container ready    |
| `includeParams` | No       | `false` | Whether to include route params in the screen event properties |

### Per-screen tracking with `useScreenTracking`

If you don't use React Navigation, or if you want fine-grained control, call `useScreenTracking` inside each screen component:

```tsx
import { useScreenTracking } from '@contentful/optimization-react-native'

function HomeScreen() {
  useScreenTracking({ name: 'Home' })
  return <View>...</View>
}
```

By default this fires once on mount. To delay tracking until data is loaded, pass `trackOnMount: false` and call `trackScreen()` manually:

```tsx
function DetailsScreen() {
  const { trackScreen } = useScreenTracking({
    name: 'Details',
    trackOnMount: false,
  })

  useEffect(() => {
    if (dataLoaded) {
      void trackScreen()
    }
  }, [dataLoaded, trackScreen])

  return <View>...</View>
}
```

### Dynamic names with `useScreenTrackingCallback`

When the screen name isn't known at render time (e.g. derived from navigation state or a deep-link), use `useScreenTrackingCallback` to get a stable callback you can fire on demand:

```tsx
import { useScreenTrackingCallback } from '@contentful/optimization-react-native'

function DynamicScreen({ screenName }: { screenName: string }) {
  const trackScreenView = useScreenTrackingCallback()

  useEffect(() => {
    trackScreenView(screenName, { source: 'deep-link' })
  }, [screenName, trackScreenView])

  return <View>...</View>
}
```

## Live updates

### Default behavior

By default, `<OptimizedEntry />` **locks to the first variant it receives** for the lifetime of the component. If a user later qualifies for a different variant mid-session (e.g. after `identify()`), they continue to see the original variant until the component unmounts. This prevents UI flashing when audience membership changes while the user is viewing content.

### Global live updates

Set `liveUpdates` on `OptimizationRoot` to enable real-time updates for every `<OptimizedEntry />` in the app:

```tsx
<OptimizationRoot clientId="your-client-id" liveUpdates>
  <YourApp />
</OptimizationRoot>
```

### Per-component live updates

Override the global setting on individual entries.

Always react to changes immediately:

```tsx
<OptimizedEntry baselineEntry={dashboard} liveUpdates>
  {(resolved) => <Dashboard entry={resolved} />}
</OptimizedEntry>
```

Lock to the first variant, even when global `liveUpdates` is enabled:

```tsx
<OptimizedEntry baselineEntry={hero} liveUpdates={false}>
  {(resolved) => <Hero entry={resolved} />}
</OptimizedEntry>
```

Inherit the global setting:

```tsx
<OptimizedEntry baselineEntry={banner}>{(resolved) => <Banner entry={resolved} />}</OptimizedEntry>
```

### Resolution priority

The effective live-updates state for a given `<OptimizedEntry />` is resolved in this order (highest to lowest priority):

1. **Preview panel open** — always forces live updates on (cannot be overridden).
2. **Component `liveUpdates` prop** — explicit per-component override.
3. **`OptimizationRoot` `liveUpdates` prop** — global setting.
4. **Default** — locked 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 |

To read the current state programmatically, use `useLiveUpdates()`:

```tsx
import { useLiveUpdates } from '@contentful/optimization-react-native'

function StatusBadge() {
  const liveUpdates = useLiveUpdates()
  const isLive = liveUpdates?.globalLiveUpdates ?? false
  return <Text>{isLive ? 'Live' : 'Locked'}</Text>
}
```

## Preview panel

The preview panel is an in-app developer surface that lets you browse audiences, override variant selection, and inspect the current profile — all without modifying real user data. It's the React Native counterpart to the Web preview panel.

### Enabling the preview panel

Pass a `previewPanel` config to `OptimizationRoot`. You must also pass an initialized Contentful client so the panel can fetch audience and experience entries:

```tsx
import { OptimizationRoot } from '@contentful/optimization-react-native'
import { createClient } from 'contentful'

const contentfulClient = createClient({
  space: 'your-space-id',
  accessToken: 'your-delivery-token',
  environment: 'main',
})

export default function App() {
  return (
    <OptimizationRoot
      clientId="your-client-id"
      previewPanel={{
        enabled: __DEV__,
        contentfulClient,
      }}
    >
      <YourApp />
    </OptimizationRoot>
  )
}
```

With `enabled: true`, a floating action button appears on top of your app. Tap it to open the panel drawer.

For real apps, gate on `__DEV__` (or another build flag) so the FAB doesn't appear in production.

### Customizing the floating action button

Use `fabPosition` and `showHeader` to fine-tune placement and chrome:

```tsx
<OptimizationRoot
  clientId="your-client-id"
  previewPanel={{
    enabled: __DEV__,
    contentfulClient,
    fabPosition: { bottom: 50, right: 20 },
    showHeader: true,
    onVisibilityChange: (visible) => console.log('preview panel visible:', visible),
  }}
>
  <YourApp />
</OptimizationRoot>
```

### Preview panel and live updates

When the preview panel is open, **all** `<OptimizedEntry />` components automatically enable live updates, regardless of their `liveUpdates` prop or the global setting. This is what makes "override audience → see variant change immediately" work in the panel without a screen reload.

You can read the current panel visibility via `useLiveUpdates()`:

```tsx
import { useLiveUpdates } from '@contentful/optimization-react-native'

function DebugBadge() {
  const liveUpdates = useLiveUpdates()
  return <Text>Preview panel: {liveUpdates?.previewPanelVisible ? 'open' : 'closed'}</Text>
}
```

## Reference implementations to compare against

* [React Native reference implementation](https://github.com/contentful/optimization/blob/main/implementations/react-native-sdk/README.md): The in-tree React Native app that is built and tested alongside the SDK itself. It demonstrates provider setup, consent bootstrap, page emission, entry rendering, scroll provider usage, `OptimizedEntry` rendering plus tap tracking, navigation tracking, and live-updates behavior.