Contentful Optimization SDK for React Native applications.
npm install @contentful/optimization-react-native @react-native-async-storage/async-storage
import { createClient } from 'contentful'
import Optimization, {
OptimizationProvider,
ScrollProvider,
Personalization,
Analytics,
} from '@contentful/optimization-react-native'
// Initialize the Contentful client
const contentful = createClient({
space: 'your-space-id',
accessToken: 'your-access-token',
})
// Initialize the Optimization SDK
const optimization = await Optimization.create({
clientId: 'your-client-id',
environment: 'your-environment',
})
// Fetch a baseline entry with full includes for personalization
const heroEntry = await contentful.getEntry('hero-baseline-id', {
include: 10, // Required to load all variant data
})
// Wrap your app with the providers
function App() {
return (
<OptimizationProvider instance={optimization}>
<ScrollProvider>
{/* For personalized entries */}
<Personalization baselineEntry={heroEntry}>
{(resolvedEntry) => (
<HeroComponent
title={resolvedEntry.fields.title}
image={resolvedEntry.fields.image}
/>
)}
</Personalization>
{/* For non-personalized entries */}
<Analytics entry={productEntry}>
<ProductCard data={productEntry.fields} />
</Analytics>
</ScrollProvider>
</OptimizationProvider>
)
}
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 <ScrollProvider>, tracking uses the actual scroll position and viewport
dimensions:
<ScrollProvider>
<Personalization baselineEntry={entry}>
{(resolvedEntry) => <HeroComponent data={resolvedEntry} />}
</Personalization>
<Analytics entry={productEntry}>
<ProductCard data={productEntry.fields} />
</Analytics>
</ScrollProvider>
Benefits:
When used without <ScrollProvider>, tracking uses screen dimensions instead:
{/* No ScrollProvider needed for non-scrollable content */}
<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:
This SDK provides all the functionality from @contentful/optimization-core plus React
Native-specific features:
@react-native-async-storage/async-storageAll core SDK features are available directly from this package:
AnalyticsStateful, AnalyticsStateless - Track user eventsPersonalization class and resolvers - Deliver personalized contentbatch, effect, signals - Reactive state managementlogger - Logging utilitiesThe <Personalization /> component handles variant resolution and tracking for personalized
Contentful entries:
import { createClient } from 'contentful'
import { Personalization } from '@contentful/optimization-react-native'
const contentful = createClient({ /* ... */ })
// Fetch baseline entry with full includes (required for personalization)
const heroEntry = await contentful.getEntry('hero-baseline-id', {
include: 10, // Loads all linked variants and experiences
})
function MyScreen() {
return (
<ScrollProvider>
<Personalization baselineEntry={heroEntry}>
{(resolvedEntry) => (
<View>
<Text>{resolvedEntry.fields.title}</Text>
<Image source={{ uri: resolvedEntry.fields.image.fields.file.url }} />
</View>
)}
</Personalization>
</ScrollProvider>
)
}
The <Analytics /> component tracks views of standard Contentful entries:
import { Analytics } from '@contentful/optimization-react-native'
const productEntry = await contentful.getEntry('product-123')
function ProductScreen() {
return (
<ScrollProvider>
<Analytics entry={productEntry}>
<ProductCard
name={productEntry.fields.name}
price={productEntry.fields.price}
image={productEntry.fields.image}
/>
</Analytics>
</ScrollProvider>
)
}
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:
ScrollProvider (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 Optimization, { useOptimization } from '@contentful/optimization-react-native'
// In a component
function MyComponent() {
const optimization = useOptimization()
const trackManually = async () => {
await optimization.analytics.trackComponentView({
componentId: 'my-component',
experienceId: 'exp-456',
variantIndex: 0,
})
}
return <Button onPress={trackManually} title="Track" />
}
import React, { useEffect, useState } from 'react'
import { View, Text, Image } from 'react-native'
import { createClient } from 'contentful'
import type { Entry } from 'contentful'
import Optimization, {
logger,
OptimizationProvider,
ScrollProvider,
Personalization,
Analytics,
type CoreConfig,
} from '@contentful/optimization-react-native'
// Initialize Contentful client
const contentful = createClient({
space: 'your-space-id',
accessToken: 'your-access-token',
})
// Use logger
logger.info('App starting')
// Optimization SDK config
const config: CoreConfig = {
clientId: 'your-client-id',
environment: 'main',
logLevel: 'info',
}
function App() {
const [optimization, setOptimization] = useState<Optimization | null>(null)
const [heroEntry, setHeroEntry] = useState<Entry | null>(null)
const [productEntry, setProductEntry] = useState<Entry | null>(null)
useEffect(() => {
// Initialize SDK and fetch entries
Promise.all([
Optimization.create(config),
contentful.getEntry('hero-baseline-id', { include: 10 }),
contentful.getEntry('product-123'),
]).then(([opt, hero, product]) => {
setOptimization(opt)
setHeroEntry(hero)
setProductEntry(product)
})
}, [])
if (!optimization || !heroEntry || !productEntry) {
return <Text>Loading...</Text>
}
return (
<OptimizationProvider instance={optimization}>
<ScrollProvider>
<View>
{/* Personalized hero section */}
<Personalization baselineEntry={heroEntry}>
{(resolvedEntry) => (
<View>
<Text>{resolvedEntry.fields.title}</Text>
<Text>{resolvedEntry.fields.description}</Text>
</View>
)}
</Personalization>
{/* Non-personalized product */}
<Analytics entry={productEntry}>
<View>
<Text>{productEntry.fields.name}</Text>
<Text>${productEntry.fields.price}</Text>
</View>
</Analytics>
</View>
</ScrollProvider>
</OptimizationProvider>
)
}
The SDK automatically configures:
'mobile''Optimization React Native SDK'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.