Contentful Personalization & Analytics
    Preparing search index...

    Integrating the Optimization React Native SDK in a React Native app

    Use this guide when you want to add personalization, analytics, screen tracking, and a preview panel to a React Native (or Expo) application using @contentful/optimization-react-native.

    Table of Contents

    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 and, when persistence consent permits it, 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.

    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 when application policy permits it, 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 this repository shows those patterns in a working application:

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

    For offline support (recommended), also install:

    pnpm add @react-native-community/netinfo
    

    The SDK uses AsyncStorage to persist consent and, when persistence consent permits it, profile state 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.

    Note

    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 documents the monorepo setup and E2E commands.

    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.

    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, optional preview panel settings, and navigation integration:

    <OptimizationRoot
    clientId={OPTIMIZATION_CLIENT_ID}
    environment={OPTIMIZATION_ENVIRONMENT}
    contentfulLocales={{
    default: 'en-US',
    supported: ['en-US', 'de-DE', 'fr-FR'],
    }}
    locale="en-US"
    logLevel={__DEV__ ? 'info' : 'warn'}
    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)
    allowedEventTypes EventType[] No ['identify', 'screen'] Event types allowed before consent is explicitly set
    logLevel LogLevels No 'error' Minimum console log level
    previewPanel PreviewPanelConfig No undefined Enables the in-app preview panel; see Preview Panel
    liveUpdates boolean No false Global live-updates default for <OptimizedEntry />
    locale string No undefined unless locale config is set Initial app/content locale candidate
    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.

    Use contentfulLocales when the same app screen renders localized Contentful entries. Configure contentfulLocales.default for single-locale apps, and add contentfulLocales.supported when the app needs device locale matching across multiple Contentful locales. Copy those codes from Contentful locale settings or the CMA locale list. The locale prop supplies the initial app/content locale. The resolved optimization.locale, when present, is the Contentful locale code used by withOptimizationLocale() and by default Experience API localization unless you provide an explicit api.locale override.

    Changing the provider locale prop after initialization calls optimization.setLocale(nextLocale) and updates optimization.locale plus optimization.states.locale. It does not fetch content or refresh profile state; call screen, identify, or CDA methods again when your app needs localized data refreshed. For the full matching rules, configuration cases, and Experience API locale behavior, see Locale handling in the Optimization SDK Suite.

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

    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.

    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.

    <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.

    React Native applications usually choose one startup policy: seed accepted consent when application policy permits Optimization by default, or leave SDK consent unset and connect application-owned controls to consent(true | false).

    If your application policy permits Optimization by default and you do not render an end-user consent prompt, set defaults.consent: true so events flow immediately:

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

    The default is applied once at startup; user input later takes precedence. It also permits durable profile-continuity storage for profile, selected optimizations, changes, and the anonymous ID. Set this default during SDK initialization rather than from a component effect so the persistence policy is in place before child effects, screen tracking, or manual identify() calls can run.

    When durable profile-continuity persistence is allowed, SDK state from an Experience response is published only after the corresponding AsyncStorage write for that same response snapshot has settled or failed gracefully. This affects durability timing, not the live source of truth: after startup hydration, SDK state and rendered SDK-derived UI read from in-memory SDK state. Wait for SDK-derived state or UI instead of adding sleeps before relaunching or terminating the app in tests.

    When your application policy depends on user choice, leave defaults.consent unset and call consent() from your banner UI:

    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.

    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 consent is granted or the event type is allow-listed. For cross-SDK consent policy guidance, see Consent management in the Optimization SDK Suite.

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

    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.

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

    optimization.consent(false)
    

    Use these options only when your application policy needs a stricter or split consent model:

    • Set allowedEventTypes={[]} for strict opt-in before any Optimization event can emit.
    • Call optimization.consent({ events: true, persistence: false }) when events are allowed but durable profile continuity must stay session-only.

    <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 and one CDA locale on Contentful's Delivery API call:

    const optimization = useOptimization()
    const contentful = optimization.withOptimizationLocale(contentfulClient)

    const cta = await contentful.getEntry(CTA_ENTRY_ID, {
    include: 10,
    })

    The React Native reference implementation centralizes this Contentful fetching pattern in its application helper layer.

    Configure contentfulLocales.default for single-locale apps, and add contentfulLocales.supported for localized apps that need device locale matching. The recommended withOptimizationLocale() helper lets Contentful entry fetches use the live resolved locale by default; data layers that need direct control can pass optimization.locale explicitly. Use optimization.setLocale(nextLocale) when the app changes language after initialization. contentful.js withAllLocales and raw CDA locale=* return locale-keyed fields; the SDK resolver expects a standard single-locale CDA entry shape where fields.nt_experiences and fields.nt_variants are direct field values. See Entry personalization and variant resolution for the entry contract and Locale handling in the Optimization SDK Suite for the broader locale model.

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

    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:

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

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

    return <CTAHeader entry={resolvedEntry} />
    }

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

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

    This is the same tracking pattern used by the React Native reference implementation: entries are wrapped so the SDK can track views/taps, while non-optimized content passes through unchanged.

    <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.

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

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

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

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

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

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

    Disable view tracking for a high-frequency entry:

    <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:

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

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

    <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.

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

    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 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.

    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.

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

    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 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

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

    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:

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

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

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

    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:

    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>
    }

    Use this optional step when your mobile app already sends events to a customer-data platform, product analytics destination, or vendor SDK. The Optimization SDK still sends events to Contentful. Your application decides which approved Contentful context, if any, should also be forwarded.

    Reporting need React Native SDK handoff
    SDK screen, custom, view, tap, or flag view Register one states.eventStream subscription from onStatesReady.
    Business event attribution Add Contentful fields in the tap handler or screen action that owns the event.
    Entry or variant attribution Use the resolved entry metadata from OptimizedEntry or the action render path.
    Custom Flag attribution Forward from the same component or hook path that reads or renders the flag.
    Consent or duplicate-delivery verification Use states.blockedEventStream, messageId dedupe, and destination debuggers.

    eventStream is a live handoff, not a replay queue. Register the subscription at provider initialization; if the analytics destination is not ready yet, buffer forwarded payloads in application code with an explicit size, TTL, and drop policy.

    Attach app-level analytics subscriptions with onStatesReady so screen, navigation, and entry effects cannot emit SDK events before the subscriber is registered:

    <OptimizationRoot
    clientId="your-client-id"
    onStatesReady={(states) => {
    const forwardedMessageIds = new Set<string>()

    const eventSubscription = states.eventStream.subscribe((event) => {
    if (!event) return
    if (forwardedMessageIds.has(event.messageId)) return
    if (!canForwardSdkEvent(event)) return

    forwardedMessageIds.add(event.messageId)

    analytics.track(`Contentful ${event.type}`, pickContentfulEventProperties(event))
    })

    return () => eventSubscription.unsubscribe()
    }}
    >
    <OptimizationNavigationContainer>
    {(navigationProps) => (
    <NavigationContainer {...navigationProps}>{/* navigators */}</NavigationContainer>
    )}
    </OptimizationNavigationContainer>
    </OptimizationRoot>

    Use Forwarding Optimization SDK context to analytics and tag-management tools for the canForwardSdkEvent and pickContentfulEventProperties helpers, vendor mappings, consent, identity, dedupe, and governance guidance.

    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.

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

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

    Override the global setting on individual entries.

    Always react to changes immediately:

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

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

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

    Inherit the global setting:

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

    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():

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

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

    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.

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

    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.

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

    <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>

    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():

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

    function DebugBadge() {
    const liveUpdates = useLiveUpdates()
    return <Text>Preview panel: {liveUpdates?.previewPanelVisible ? 'open' : 'closed'}</Text>
    }
    • React Native reference implementation: 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.