Migrate experience.js React to the React Web SDK
Overview
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.
What changes
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.
Before you migrate
Gather these inputs:
- The root where
NinetailedProvideris mounted and any existing injected client. - Every
Personalize,Experience,usePersonalize,useExperience,useProfile,useFlag,useFlagWithManualTracking, andTrackHasSeenComponentcall. - Router page-tracking code, including whether the app uses React Router, TanStack Router, a custom router, or no router abstraction.
- Consent, identity, and analytics policy.
- Contentful entries that still depend on legacy mapper output or
nt_*fields.
Migration path
- Migrate Contentful authoring when legacy
nt_*fields feed React rendering. See Migrate an experience.js Contentful model to Optimization. - Install
@contentful/optimization-react-web. - Replace root wiring with
OptimizationRootand the appropriate router page tracker: React Router, TanStack Router, or an app-ownedtrackPageViewcall for a custom router. - Replace personalized rendering with
OptimizedEntryor target entry hooks. - Replace flags, interactions, consent, identity, analytics, and preview through target surfaces.
- Remove
@ninetailed/experience.js-reactand legacy browser packages after imports are gone.
Replace legacy surfaces
Inventory provider, hooks, and render components
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.
Replace the provider and readiness model
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 personalized entry rendering
Replace legacy component and hook rendering with target entry resolution:
- Use
OptimizedEntrywhen a component renders a Contentful entry. - Use
useOptimizedEntryorresolveOptimizedEntry()when you need custom control. - Keep your existing presentational components; render the resolved entry through them.
Use one entry source per component:
- Use
baselineEntrywhen your app already fetched the Contentful entry and owns the query, caching, and include depth. - Use
entryIdonly whenOptimizationRoothas acontentfulclient 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.
Replace flags and tracking side effects
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.
Replace consent, identity, analytics, and preview
Move policy and vendor work out of the core render migration:
- Consent uses app-owned state passed to the SDK through target consent actions or defaults.
- Identity uses target
identifyUserandresetUseractions. - Analytics vendors subscribe to accepted and blocked event streams instead of legacy plugins.
- Preview uses the Optimization preview panel package attached to the live Web SDK.
Use Migrating experience.js plugins and preview for those replacements.
Validate React migration
Verify these outcomes before deleting the legacy packages:
- The React root mounts one Optimization SDK runtime.
- One page event is accepted or intentionally blocked by policy.
- A target
OptimizedEntryrenders the authored variant or baseline fallback. - Interaction and flag-view events follow the target consent policy.
- No legacy React component or hook imports remain.
Validate the migration
- Search for
@ninetailed/experience.js-react,Personalize,Experience,usePersonalize,useExperience,useFlag, andTrackHasSeenComponent. - Verify the app renders the same baseline entry with no matching variant.
- Verify an all-visitors variant renders after the page event.
- Verify accepted events and blocked diagnostics through the target SDK streams.