Use this guide when a React app uses @ninetailed/experience.js-react providers, hooks,
components, or flags and you want to move to @contentful/optimization-react-web.
Legacy React surfaces wrap the browser Ninetailed client and render through Personalize,
Experience, usePersonalize, useExperience, and flag hooks. React Web uses
OptimizationRoot, target hooks, and OptimizedEntry over the Optimization content model. The app
keeps Contentful fetching, consent policy, identity policy, routing, analytics, and rendering
components.
Follow the React Web integration guide for the target provider and rendering setup.
Gather these inputs:
NinetailedProvider is mounted and any existing injected client.Personalize, Experience, usePersonalize, useExperience, useProfile, useFlag,
useFlagWithManualTracking, and TrackHasSeenComponent call.nt_* fields.nt_* fields feed React rendering. See
Migrating an experience.js Contentful model to Optimization.@contentful/optimization-react-web.OptimizationRoot and the appropriate router page tracker: React
Router, TanStack Router, or an app-owned trackPageView call for a custom router.OptimizedEntry or target entry hooks.@ninetailed/experience.js-react and legacy browser packages after imports are gone.Separate root setup from component-level rendering. Root setup includes NinetailedProvider,
plugins, page tracking, consent defaults, and any injected client. Component-level rendering
includes every Personalize, Experience, hook, flag read, and tracking wrapper.
This split keeps the first migration step small: get the target root and page event working before rewriting every personalized component.
Mount one OptimizationRoot around the tree that uses personalization. React Web creates the
underlying Web SDK after React commits, so readiness and loading state matter. Provider children
still render; optimized entries handle baseline and loading behavior at the entry boundary.
Do not rely on window.ninetailed or legacy provider prop changes. The target root owns the Web SDK
instance for the browser runtime, and app policy owns consent defaults.
Replace legacy component and hook rendering with target entry resolution:
OptimizedEntry when a component renders a Contentful entry.useOptimizedEntry or resolveOptimizedEntry() when you need custom control.Use one entry source per component:
baselineEntry when your app already fetched the Contentful entry and owns the query,
caching, and include depth.entryId only when OptimizationRoot has a contentful client for managed fetching.Both paths return the baseline when no selection matches or linked entries are missing. If legacy mapper code still builds experience arrays, migrate the Contentful model first.
Do not preserve legacy flag hook names as wrappers unless your app needs a temporary compatibility layer. Use the target SDK flag APIs:
sdk.getFlag() for a one-off read.sdk.states.flag(name) for a reactive read.sdk.trackFlagView() when you need explicit manual flag-view tracking.React Web optimized entries track views, clicks, and hovers by default when consent and profile
state allow them. Accepted interaction events appear on the live SDK's states.eventStream; consent
blocks appear on states.blockedEventStream. If a legacy wrapper existed only to track component
views, replace it with OptimizedEntry tracking rather than a custom component.
Move policy and vendor work out of the core render migration:
identifyUser and resetUser actions.Use Migrating experience.js plugins and preview for those replacements.
Verify these outcomes before deleting the legacy packages:
OptimizedEntry renders the authored variant or baseline fallback.@ninetailed/experience.js-react, Personalize, Experience, usePersonalize,
useExperience, useFlag, and TrackHasSeenComponent.| Symptom | Check |
|---|---|
| The first render has no SDK state | React Web initializes after commit; use entry loading and baseline behavior instead of reading live state during root setup. |
OptimizedEntry renders baseline |
Confirm the Contentful payload includes linked Optimization entries and that a page or identify event populated selections. |
| Interaction events are missing | Check consent, profile availability, root/per-entry tracking opt-outs, and whether the rendered node carries target tracking attributes. |
| Preview does not attach | Attach the preview panel to the live Web SDK, not the initial snapshot runtime. |