From d0b0cd9d7f469fb5a8948f1619fc03835c4dbf91 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 18:24:44 +0100 Subject: [PATCH 01/13] feat: Add LiveUpdatesProvider and update OptimizationProvider context wiring --- .../src/provider/LiveUpdatesProvider.tsx | 21 +++++++++++++++++++ .../src/provider/OptimizationProvider.tsx | 15 ++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx new file mode 100644 index 00000000..8000c2e8 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx @@ -0,0 +1,21 @@ +import { PropsWithChildren, ReactElement, useContext } from 'react' +import { LiveUpdatesContext, LiveUpdatesContextValue } from '../context/LiveUpdatesContext' + +export interface LiveUpdatesProviderProps extends PropsWithChildren { + readonly globalLiveUpdates?: boolean +} + +export function LiveUpdatesProvider({ + children, + globalLiveUpdates = false, +}: LiveUpdatesProviderProps): ReactElement { + return ( + + {children} + + ) +} + +export function useLiveUpdates(): LiveUpdatesContextValue | null { + return useContext(LiveUpdatesContext) +} diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx index ae8beae9..1358add7 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx @@ -1,12 +1,17 @@ import type { PropsWithChildren, ReactElement } from 'react' -import type { OptimizationWebSdkOrNull } from '../types' +import { OptimizationContext } from '../context/OptimizationContext' +import type { OptimizationWebSdk } from '../types' export interface OptimizationProviderProps extends PropsWithChildren { - readonly optimization?: OptimizationWebSdkOrNull + readonly instance: OptimizationWebSdk } -export function OptimizationProvider({ children }: OptimizationProviderProps): ReactElement { - // Scaffold placeholder: context wiring will be implemented in follow-up tickets. - return <>{children} +export function OptimizationProvider({ + children, + instance, +}: OptimizationProviderProps): ReactElement { + return ( + {children} + ) } From fa6097635b7b56d6a3467294a886ae93ea486613 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 18:25:03 +0100 Subject: [PATCH 02/13] feat: Add LiveUpdatesContext and OptimizationContext --- .../react-web/src/context/LiveUpdatesContext.tsx | 7 +++++++ .../react-web/src/context/OptimizationContext.tsx | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 platforms/javascript/web-frameworks/react-web/src/context/LiveUpdatesContext.tsx create mode 100644 platforms/javascript/web-frameworks/react-web/src/context/OptimizationContext.tsx diff --git a/platforms/javascript/web-frameworks/react-web/src/context/LiveUpdatesContext.tsx b/platforms/javascript/web-frameworks/react-web/src/context/LiveUpdatesContext.tsx new file mode 100644 index 00000000..d2eed27c --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/context/LiveUpdatesContext.tsx @@ -0,0 +1,7 @@ +import { createContext } from 'react' + +export interface LiveUpdatesContextValue { + readonly globalLiveUpdates: boolean +} + +export const LiveUpdatesContext = createContext(null) diff --git a/platforms/javascript/web-frameworks/react-web/src/context/OptimizationContext.tsx b/platforms/javascript/web-frameworks/react-web/src/context/OptimizationContext.tsx new file mode 100644 index 00000000..114e594b --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/context/OptimizationContext.tsx @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +import type { OptimizationWebSdk } from '../types' + +export interface OptimizationContextValue { + readonly instance: OptimizationWebSdk +} + +export const OptimizationContext = createContext(null) From 70a4d5b09e476061e6edc4d85d48c456edd712e9 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 18:25:22 +0100 Subject: [PATCH 03/13] feat: Add useLiveUpdates and update useOptimization hooks --- .../react-web/src/hooks/useLiveUpdates.ts | 15 ++++++++++++ .../react-web/src/hooks/useOptimization.ts | 23 +++++++++++-------- 2 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts diff --git a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts new file mode 100644 index 00000000..28260393 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' +import { LiveUpdatesContext, LiveUpdatesContextValue } from '../context/LiveUpdatesContext' + +export function useLiveUpdates(): LiveUpdatesContextValue | null { + const context = useContext(LiveUpdatesContext) + + if (!context) { + throw new Error( + 'useLiveUpdates must be used within a LiveUpdatesProvider. ' + + 'Make sure to wrap your component tree with .', + ) + } + + return context +} diff --git a/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts b/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts index bfd10394..e35d1ff8 100644 --- a/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts +++ b/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts @@ -1,14 +1,17 @@ -import type { OptimizationWebSdkOrNull } from '../types' +import { useContext } from 'react' -export interface UseOptimizationResult { - readonly optimization: OptimizationWebSdkOrNull - readonly isReady: boolean -} +import { OptimizationContext } from '../context/OptimizationContext' +import type { OptimizationWebSdk } from '../types' + +export function useOptimization(): OptimizationWebSdk { + const context = useContext(OptimizationContext) -export function useOptimization(): UseOptimizationResult { - // Scaffold placeholder: reads from provider context will be added later. - return { - optimization: null, - isReady: false, + if (!context) { + throw new Error( + 'useOptimization must be used within an OptimizationProvider. ' + + 'Make sure to wrap your component tree with .', + ) } + + return context.instance } From 7e079b087645b37467ecff3b9ebc305cdcf04bd0 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 18:29:50 +0100 Subject: [PATCH 04/13] feat: Add LiveUpdatesProvider to OptimizationRoot and update exports --- .../web-frameworks/react-web/src/index.ts | 8 +++++++- .../react-web/src/root/OptimizationRoot.tsx | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/src/index.ts b/platforms/javascript/web-frameworks/react-web/src/index.ts index 26d1e543..ecf3eca0 100644 --- a/platforms/javascript/web-frameworks/react-web/src/index.ts +++ b/platforms/javascript/web-frameworks/react-web/src/index.ts @@ -1,9 +1,15 @@ export { useAnalytics } from './analytics/useAnalytics' export type { UseAnalyticsResult } from './analytics/useAnalytics' +export { LiveUpdatesContext } from './context/LiveUpdatesContext' +export type { LiveUpdatesContextValue } from './context/LiveUpdatesContext' +export { OptimizationContext } from './context/OptimizationContext' +export type { OptimizationContextValue } from './context/OptimizationContext' +export { useLiveUpdates } from './hooks/useLiveUpdates' export { useOptimization } from './hooks/useOptimization' -export type { UseOptimizationResult } from './hooks/useOptimization' export { usePersonalization } from './personalization/usePersonalization' export type { UsePersonalizationResult } from './personalization/usePersonalization' +export { LiveUpdatesProvider } from './provider/LiveUpdatesProvider' +export type { LiveUpdatesProviderProps } from './provider/LiveUpdatesProvider' export { OptimizationProvider } from './provider/OptimizationProvider' export type { OptimizationProviderProps } from './provider/OptimizationProvider' export { OptimizationRoot } from './root/OptimizationRoot' diff --git a/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx b/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx index 68fa8c1f..07af1774 100644 --- a/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx @@ -1,12 +1,22 @@ import type { PropsWithChildren, ReactElement } from 'react' +import { LiveUpdatesProvider } from '../provider/LiveUpdatesProvider' import { OptimizationProvider } from '../provider/OptimizationProvider' -import type { OptimizationWebSdkOrNull } from '../types' +import type { OptimizationWebSdk } from '../types' export interface OptimizationRootProps extends PropsWithChildren { - readonly optimization?: OptimizationWebSdkOrNull + readonly instance: OptimizationWebSdk + readonly liveUpdates?: boolean } -export function OptimizationRoot({ children, optimization }: OptimizationRootProps): ReactElement { - return {children} +export function OptimizationRoot({ + children, + instance, + liveUpdates = false, +}: OptimizationRootProps): ReactElement { + return ( + + {children} + + ) } From f6fb87b0d669627206a445873c9cd1dec585351d Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 18:30:02 +0100 Subject: [PATCH 05/13] feat: Replace scaffold test with core provider tests --- .../react-web/src/index.test.ts | 31 ----- .../react-web/src/index.test.tsx | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 31 deletions(-) delete mode 100644 platforms/javascript/web-frameworks/react-web/src/index.test.ts create mode 100644 platforms/javascript/web-frameworks/react-web/src/index.test.tsx diff --git a/platforms/javascript/web-frameworks/react-web/src/index.test.ts b/platforms/javascript/web-frameworks/react-web/src/index.test.ts deleted file mode 100644 index c30a4891..00000000 --- a/platforms/javascript/web-frameworks/react-web/src/index.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - OptimizationProvider, - OptimizationRoot, - useAnalytics, - useOptimization, - usePersonalization, -} from './index' - -describe('@contentful/optimization-react-web scaffold', () => { - it('exports scaffold API symbols', () => { - expect(OptimizationProvider).toBeTypeOf('function') - expect(OptimizationRoot).toBeTypeOf('function') - expect(useOptimization).toBeTypeOf('function') - expect(usePersonalization).toBeTypeOf('function') - expect(useAnalytics).toBeTypeOf('function') - }) - - it('returns inert placeholder values', async () => { - const optimization = useOptimization() - const personalization = usePersonalization() - const analytics = useAnalytics() - - expect(optimization.isReady).toBe(false) - expect(optimization.optimization).toBeNull() - expect(personalization.resolveEntry({ id: 'entry-1' })).toEqual({ id: 'entry-1' }) - - await expect(analytics.identify('user-1')).resolves.toBeUndefined() - await expect(analytics.track({ event: 'view' })).resolves.toBeUndefined() - await expect(analytics.reset()).resolves.toBeUndefined() - }) -}) diff --git a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx new file mode 100644 index 00000000..b88fb760 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx @@ -0,0 +1,129 @@ +import { + LiveUpdatesProvider, + OptimizationContext, + OptimizationProvider, + OptimizationRoot, + useAnalytics, + useLiveUpdates, + useOptimization, + usePersonalization, +} from './index' +import { renderToString } from 'react-dom/server' +import type { OptimizationWebSdk } from './types' + +const optimizationInstance = { sdk: 'test-instance' } as unknown as OptimizationWebSdk + +describe('@contentful/optimization-react-web core providers', () => { + it('exports core API symbols', () => { + expect(OptimizationContext).toBeDefined() + expect(LiveUpdatesProvider).toBeTypeOf('function') + expect(OptimizationProvider).toBeTypeOf('function') + expect(OptimizationRoot).toBeTypeOf('function') + expect(useOptimization).toBeTypeOf('function') + expect(useLiveUpdates).toBeTypeOf('function') + expect(usePersonalization).toBeTypeOf('function') + expect(useAnalytics).toBeTypeOf('function') + }) + + it('provides optimization instance via OptimizationProvider', () => { + let capturedInstance: OptimizationWebSdk | null = null + + function Probe(): null { + capturedInstance = useOptimization() + return null + } + + renderToString( + + + , + ) + + expect(capturedInstance).toBe(optimizationInstance) + }) + + it('throws actionable error when useOptimization is called outside provider', () => { + function BrokenProbe(): null { + useOptimization() + return null + } + + expect(() => renderToString()).toThrow( + 'useOptimization must be used within an OptimizationProvider', + ) + expect(() => renderToString()).toThrow( + '', + ) + }) + + it('defaults liveUpdates to false in OptimizationRoot', () => { + let capturedGlobalLiveUpdates: boolean | null = null + + function Probe(): null { + capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? false + return null + } + + renderToString( + + + , + ) + + expect(capturedGlobalLiveUpdates).toBe(false) + }) + + it('passes global live updates through context from OptimizationRoot', () => { + let capturedGlobalLiveUpdates: boolean | null = null + + function Probe(): null { + capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? null + return null + } + + renderToString( + + + , + ) + + expect(capturedGlobalLiveUpdates).toBe(true) + }) + + it('supports live updates fallback semantics for dependent components', () => { + const results: boolean[] = [] + + function Probe({ liveUpdates }: { liveUpdates?: boolean }): null { + const context = useLiveUpdates() + const isLive = liveUpdates ?? context?.globalLiveUpdates ?? false + results.push(isLive) + return null + } + + renderToString( + + + + , + ) + + renderToString( + + + , + ) + + expect(results).toEqual([true, false, true]) + }) + + it('keeps non-core hooks inert placeholders for now', async () => { + const personalization = usePersonalization() + const analytics = useAnalytics() + + expect(personalization.resolveEntry({ id: 'entry-1' })).toEqual({ id: 'entry-1' }) + + await expect(analytics.identify('user-1')).resolves.toBeUndefined() + await expect(analytics.track({ event: 'view' })).resolves.toBeUndefined() + await expect(analytics.reset()).resolves.toBeUndefined() + }) +}) From 3fc02f64d45f9de5f785b9667d09d927e4569219 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 18:30:15 +0100 Subject: [PATCH 06/13] docs: Update README with core provider and usage details --- .../web-frameworks/react-web/README.md | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/README.md b/platforms/javascript/web-frameworks/react-web/README.md index 4d4824aa..0ababe3e 100644 --- a/platforms/javascript/web-frameworks/react-web/README.md +++ b/platforms/javascript/web-frameworks/react-web/README.md @@ -1,13 +1,14 @@ # React Web SDK -Scaffold package for `@contentful/optimization-react-web`. +React Web SDK package for `@contentful/optimization-react-web`. ## Status -This package is scaffold-only and not production-ready. +Core root/provider primitives are implemented. -- Runtime behavior is intentionally out of scope for this initial setup. -- API semantics and React Native parity details are TODO in follow-up tickets. +- `OptimizationProvider` + `useOptimization()` context behavior +- `LiveUpdatesProvider` + `useLiveUpdates()` global live updates context +- `OptimizationRoot` provider composition and defaults ## Purpose @@ -38,5 +39,51 @@ pnpm dev - package metadata and dual module exports - `rslib`/`rsbuild`/`rstest`/TypeScript baseline aligned with Web SDK patterns -- placeholder React-facing API surface in `src/` (provider/root/personalization/analytics/hooks) +- core provider/root/context primitives in `src/` - scaffold dev dashboard harness in `dev/` for consent, identify/reset, state, events, and entries + +## Usage + +### Recommended Wrapper + +Use `OptimizationRoot` as the standard top-level wrapper: + +```tsx +import { OptimizationRoot } from '@contentful/optimization-react-web' + + + + +``` + +`OptimizationRoot` composition order: + +1. `OptimizationProvider` (outermost) +2. `LiveUpdatesProvider` +3. application children + +### Provider Requirements + +- `OptimizationProvider` requires `instance`. +- `OptimizationRoot` requires `instance`. +- `OptimizationRoot.liveUpdates` is optional and defaults to `false`. + +### Hooks + +- `useOptimization()` returns the current SDK instance. +- `useOptimization()` throws if used outside `OptimizationProvider`. +- `useLiveUpdates()` returns the live updates context or `null` outside `LiveUpdatesProvider`. + +### Live Updates Resolution Semantics + +Consumers should resolve live updates behavior with: + +```ts +const isLiveUpdatesEnabled = componentLiveUpdates ?? liveUpdatesContext?.globalLiveUpdates ?? false +``` + +This gives: + +- component-level `liveUpdates` prop override first +- then root-level `liveUpdates` +- then default `false` From 66cb8f18386e4ab7024d48e196444863dfac1006 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 19:50:06 +0100 Subject: [PATCH 07/13] feat: Relax useLiveUpdates requirement to allow null context --- .../react-web/src/hooks/useLiveUpdates.ts | 11 +------ .../react-web/src/index.test.tsx | 33 +++++++++++++++++++ .../src/provider/LiveUpdatesProvider.tsx | 8 ++--- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts index 28260393..d087289f 100644 --- a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts +++ b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts @@ -2,14 +2,5 @@ import { useContext } from 'react' import { LiveUpdatesContext, LiveUpdatesContextValue } from '../context/LiveUpdatesContext' export function useLiveUpdates(): LiveUpdatesContextValue | null { - const context = useContext(LiveUpdatesContext) - - if (!context) { - throw new Error( - 'useLiveUpdates must be used within a LiveUpdatesProvider. ' + - 'Make sure to wrap your component tree with .', - ) - } - - return context + return useContext(LiveUpdatesContext) } diff --git a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx index b88fb760..0f16221e 100644 --- a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx @@ -90,6 +90,39 @@ describe('@contentful/optimization-react-web core providers', () => { expect(capturedGlobalLiveUpdates).toBe(true) }) + it('returns null from useLiveUpdates outside provider', () => { + let capturedContext: ReturnType = undefined as never + + function Probe(): null { + capturedContext = useLiveUpdates() + return null + } + + renderToString() + + expect(capturedContext).toBeNull() + }) + + it('provides both optimization instance and live updates via OptimizationRoot', () => { + let capturedInstance: OptimizationWebSdk | null = null + let capturedGlobalLiveUpdates: boolean | null = null + + function Probe(): null { + capturedInstance = useOptimization() + capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? null + return null + } + + renderToString( + + + , + ) + + expect(capturedInstance).toBe(optimizationInstance) + expect(capturedGlobalLiveUpdates).toBe(true) + }) + it('supports live updates fallback semantics for dependent components', () => { const results: boolean[] = [] diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx index 8000c2e8..f99a01fc 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx @@ -1,5 +1,5 @@ -import { PropsWithChildren, ReactElement, useContext } from 'react' -import { LiveUpdatesContext, LiveUpdatesContextValue } from '../context/LiveUpdatesContext' +import { PropsWithChildren, ReactElement } from 'react' +import { LiveUpdatesContext } from '../context/LiveUpdatesContext' export interface LiveUpdatesProviderProps extends PropsWithChildren { readonly globalLiveUpdates?: boolean @@ -15,7 +15,3 @@ export function LiveUpdatesProvider({ ) } - -export function useLiveUpdates(): LiveUpdatesContextValue | null { - return useContext(LiveUpdatesContext) -} From 04cced14b50def67a1f34d786eff8601ce4b795d Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Mon, 2 Mar 2026 20:17:31 +0100 Subject: [PATCH 08/13] feat: Add live SDK integration and improve type usage in React Web demo --- .../web-frameworks/react-web/README.md | 3 +- .../web-frameworks/react-web/dev/App.tsx | 14 +++++- .../web-frameworks/react-web/dev/main.tsx | 47 ++++++++++++++++++- .../react-web/src/hooks/useLiveUpdates.ts | 2 +- .../react-web/src/index.test.tsx | 17 ++++--- .../src/provider/LiveUpdatesProvider.tsx | 2 +- 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/README.md b/platforms/javascript/web-frameworks/react-web/README.md index 0ababe3e..e24c6c07 100644 --- a/platforms/javascript/web-frameworks/react-web/README.md +++ b/platforms/javascript/web-frameworks/react-web/README.md @@ -50,8 +50,7 @@ Use `OptimizationRoot` as the standard top-level wrapper: ```tsx import { OptimizationRoot } from '@contentful/optimization-react-web' - - +; ``` diff --git a/platforms/javascript/web-frameworks/react-web/dev/App.tsx b/platforms/javascript/web-frameworks/react-web/dev/App.tsx index dcf103d8..e1dc30f4 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/App.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/App.tsx @@ -1,4 +1,5 @@ import type { ReactElement } from 'react' +import { useLiveUpdates, useOptimization } from '../src' const sectionTitles = [ 'Consent', @@ -10,13 +11,24 @@ const sectionTitles = [ ] as const export function App(): ReactElement { + useOptimization() + const liveUpdates = useLiveUpdates()?.globalLiveUpdates ?? false + return (

@contentful/optimization-react-web

-

Scaffold dashboard for React Web SDK development.

+

Minimal live integration with a real Optimization SDK instance.

+
+
+

SDK Wiring

+

Optimization instance: READY

+

{`Global liveUpdates: ${liveUpdates ? 'ON' : 'OFF'}`}

+
+
+
{sectionTitles.map((title) => (
diff --git a/platforms/javascript/web-frameworks/react-web/dev/main.tsx b/platforms/javascript/web-frameworks/react-web/dev/main.tsx index 4f4dffc3..f521c7b6 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/main.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/main.tsx @@ -1,17 +1,62 @@ +import Optimization from '@contentful/optimization-web' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { OptimizationRoot } from '../src' import { App } from './App' import './styles.css' const rootElement = document.getElementById('root') +const DEFAULT_CLIENT_ID = 'mock-client-id' +const DEFAULT_ENVIRONMENT = 'main' +const DEFAULT_INSIGHTS_BASE_URL = 'http://localhost:8000/insights/' +const DEFAULT_EXPERIENCE_BASE_URL = 'http://localhost:8000/experience/' if (!rootElement) { throw new Error('Missing #root element') } +function getEnvString(key: string): string | undefined { + const value: unknown = Reflect.get(import.meta.env as object, key) + + if (typeof value !== 'string') { + return undefined + } + + const normalized = value.trim() + return normalized.length > 0 ? normalized : undefined +} + +function createOptimization(): Optimization { + const clientId = getEnvString('PUBLIC_NINETAILED_CLIENT_ID') ?? DEFAULT_CLIENT_ID + const environment = getEnvString('PUBLIC_NINETAILED_ENVIRONMENT') ?? DEFAULT_ENVIRONMENT + const insightsBaseUrl = getEnvString('PUBLIC_INSIGHTS_API_BASE_URL') ?? DEFAULT_INSIGHTS_BASE_URL + const experienceBaseUrl = + getEnvString('PUBLIC_EXPERIENCE_API_BASE_URL') ?? DEFAULT_EXPERIENCE_BASE_URL + + return new Optimization({ + clientId, + environment, + analytics: { baseUrl: insightsBaseUrl }, + personalization: { baseUrl: experienceBaseUrl }, + logLevel: 'debug', + }) +} + +function getOptimization(): Optimization { + if (typeof window !== 'undefined' && window.optimization) { + return window.optimization + } + + return createOptimization() +} + +const optimization = getOptimization() + createRoot(rootElement).render( - + + + , ) diff --git a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts index d087289f..fd7b064b 100644 --- a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts +++ b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts @@ -1,5 +1,5 @@ import { useContext } from 'react' -import { LiveUpdatesContext, LiveUpdatesContextValue } from '../context/LiveUpdatesContext' +import { LiveUpdatesContext, type LiveUpdatesContextValue } from '../context/LiveUpdatesContext' export function useLiveUpdates(): LiveUpdatesContextValue | null { return useContext(LiveUpdatesContext) diff --git a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx index 0f16221e..a312fdd1 100644 --- a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx @@ -1,3 +1,5 @@ +import Optimization from '@contentful/optimization-web' +import { renderToString } from 'react-dom/server' import { LiveUpdatesProvider, OptimizationContext, @@ -8,10 +10,13 @@ import { useOptimization, usePersonalization, } from './index' -import { renderToString } from 'react-dom/server' -import type { OptimizationWebSdk } from './types' -const optimizationInstance = { sdk: 'test-instance' } as unknown as OptimizationWebSdk +const optimizationInstance = new Optimization({ + clientId: 'test-client-id', + environment: 'main', + analytics: { baseUrl: 'http://localhost:8000/insights/' }, + personalization: { baseUrl: 'http://localhost:8000/experience/' }, +}) describe('@contentful/optimization-react-web core providers', () => { it('exports core API symbols', () => { @@ -26,7 +31,7 @@ describe('@contentful/optimization-react-web core providers', () => { }) it('provides optimization instance via OptimizationProvider', () => { - let capturedInstance: OptimizationWebSdk | null = null + let capturedInstance: Optimization | null = null function Probe(): null { capturedInstance = useOptimization() @@ -91,7 +96,7 @@ describe('@contentful/optimization-react-web core providers', () => { }) it('returns null from useLiveUpdates outside provider', () => { - let capturedContext: ReturnType = undefined as never + let capturedContext: ReturnType = null function Probe(): null { capturedContext = useLiveUpdates() @@ -104,7 +109,7 @@ describe('@contentful/optimization-react-web core providers', () => { }) it('provides both optimization instance and live updates via OptimizationRoot', () => { - let capturedInstance: OptimizationWebSdk | null = null + let capturedInstance: Optimization | null = null let capturedGlobalLiveUpdates: boolean | null = null function Probe(): null { diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx index f99a01fc..54cba874 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/LiveUpdatesProvider.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, ReactElement } from 'react' +import type { PropsWithChildren, ReactElement } from 'react' import { LiveUpdatesContext } from '../context/LiveUpdatesContext' export interface LiveUpdatesProviderProps extends PropsWithChildren { From f9d9a01ea7b4b3574aef893c0e68ca6027e0cc2e Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Tue, 3 Mar 2026 13:32:32 +0100 Subject: [PATCH 09/13] refactor: OptimizationRoot to accept config props instead of instance - Update OptimizationRoot and OptimizationProvider to accept SDK config props (clientId, environment, analytics, etc.) instead of an instance - Update README and usage docs to reflect new initialization pattern - Update dev and test apps to use config-as-props pattern - Improve error messages and test coverage for provider usage - Remove legacy instance-based initialization code --- .../web-frameworks/react-web/README.md | 72 ++++++-- .../web-frameworks/react-web/dev/App.tsx | 4 +- .../web-frameworks/react-web/dev/main.tsx | 45 ++--- .../react-web/src/hooks/useOptimization.ts | 2 +- .../react-web/src/index.test.tsx | 165 +++++++++++------- .../src/provider/OptimizationProvider.tsx | 35 +++- .../react-web/src/root/OptimizationRoot.tsx | 15 +- 7 files changed, 219 insertions(+), 119 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/README.md b/platforms/javascript/web-frameworks/react-web/README.md index e24c6c07..01e5e3a0 100644 --- a/platforms/javascript/web-frameworks/react-web/README.md +++ b/platforms/javascript/web-frameworks/react-web/README.md @@ -44,29 +44,50 @@ pnpm dev ## Usage -### Recommended Wrapper +### Initialization -Use `OptimizationRoot` as the standard top-level wrapper: +Pass configuration props directly to `OptimizationRoot` (recommended) or `OptimizationProvider`. The +SDK is initialized internally by the provider. ```tsx import { OptimizationRoot } from '@contentful/optimization-react-web' -; - - + +function App() { + return ( + + + + ) +} ``` +Available config props: + +| Prop | Type | Required | Description | +| --------------------------- | ----------------------------------- | -------- | ------------------------------------------------ | +| `clientId` | `string` | Yes | Your Contentful Optimization client identifier | +| `environment` | `string` | No | Contentful environment (defaults to `'main'`) | +| `analytics` | `CoreStatefulAnalyticsConfig` | No | Analytics/Insights API configuration | +| `personalization` | `CoreStatefulPersonalizationConfig` | No | Personalization/Experience API configuration | +| `app` | `App` | No | Application metadata for events | +| `autoTrackEntryInteraction` | `AutoTrackEntryInteractionOptions` | No | Automatic entry interaction tracking options | +| `logLevel` | `LogLevels` | No | Minimum log level for console output | +| `liveUpdates` | `boolean` | No | Enable global live updates (defaults to `false`) | + +### Provider Composition + `OptimizationRoot` composition order: 1. `OptimizationProvider` (outermost) 2. `LiveUpdatesProvider` 3. application children -### Provider Requirements - -- `OptimizationProvider` requires `instance`. -- `OptimizationRoot` requires `instance`. -- `OptimizationRoot.liveUpdates` is optional and defaults to `false`. - ### Hooks - `useOptimization()` returns the current SDK instance. @@ -86,3 +107,32 @@ This gives: - component-level `liveUpdates` prop override first - then root-level `liveUpdates` - then default `false` + +## Singleton Behavior + +The underlying `@contentful/optimization-web` SDK enforces a singleton pattern. Only one +`Optimization` runtime can exist at a time (attached to `window.optimization`). Attempting to +initialize a second runtime will throw an error. + +When using the config-as-props pattern, the provider uses a `useRef` to ensure the instance is only +created once, even across React re-renders or StrictMode double-rendering. + +## Testing + +When testing components that use the Optimization providers, pass test config props: + +```tsx +import { render } from '@testing-library/react' +import { OptimizationRoot } from '@contentful/optimization-react-web' + +render( + + + , +) +``` diff --git a/platforms/javascript/web-frameworks/react-web/dev/App.tsx b/platforms/javascript/web-frameworks/react-web/dev/App.tsx index e1dc30f4..ebc88a21 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/App.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/App.tsx @@ -18,13 +18,13 @@ export function App(): ReactElement {

@contentful/optimization-react-web

-

Minimal live integration with a real Optimization SDK instance.

+

Minimal live integration with OptimizationRoot config props.

SDK Wiring

-

Optimization instance: READY

+

Optimization SDK: READY

{`Global liveUpdates: ${liveUpdates ? 'ON' : 'OFF'}`}

diff --git a/platforms/javascript/web-frameworks/react-web/dev/main.tsx b/platforms/javascript/web-frameworks/react-web/dev/main.tsx index f521c7b6..c4a8f6ab 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/main.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/main.tsx @@ -1,4 +1,3 @@ -import Optimization from '@contentful/optimization-web' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { OptimizationRoot } from '../src' @@ -6,16 +5,11 @@ import { OptimizationRoot } from '../src' import { App } from './App' import './styles.css' -const rootElement = document.getElementById('root') const DEFAULT_CLIENT_ID = 'mock-client-id' const DEFAULT_ENVIRONMENT = 'main' const DEFAULT_INSIGHTS_BASE_URL = 'http://localhost:8000/insights/' const DEFAULT_EXPERIENCE_BASE_URL = 'http://localhost:8000/experience/' -if (!rootElement) { - throw new Error('Missing #root element') -} - function getEnvString(key: string): string | undefined { const value: unknown = Reflect.get(import.meta.env as object, key) @@ -27,35 +21,28 @@ function getEnvString(key: string): string | undefined { return normalized.length > 0 ? normalized : undefined } -function createOptimization(): Optimization { - const clientId = getEnvString('PUBLIC_NINETAILED_CLIENT_ID') ?? DEFAULT_CLIENT_ID - const environment = getEnvString('PUBLIC_NINETAILED_ENVIRONMENT') ?? DEFAULT_ENVIRONMENT - const insightsBaseUrl = getEnvString('PUBLIC_INSIGHTS_API_BASE_URL') ?? DEFAULT_INSIGHTS_BASE_URL - const experienceBaseUrl = - getEnvString('PUBLIC_EXPERIENCE_API_BASE_URL') ?? DEFAULT_EXPERIENCE_BASE_URL - - return new Optimization({ - clientId, - environment, - analytics: { baseUrl: insightsBaseUrl }, - personalization: { baseUrl: experienceBaseUrl }, - logLevel: 'debug', - }) -} +const clientId = getEnvString('PUBLIC_NINETAILED_CLIENT_ID') ?? DEFAULT_CLIENT_ID +const environment = getEnvString('PUBLIC_NINETAILED_ENVIRONMENT') ?? DEFAULT_ENVIRONMENT +const insightsBaseUrl = getEnvString('PUBLIC_INSIGHTS_API_BASE_URL') ?? DEFAULT_INSIGHTS_BASE_URL +const experienceBaseUrl = + getEnvString('PUBLIC_EXPERIENCE_API_BASE_URL') ?? DEFAULT_EXPERIENCE_BASE_URL -function getOptimization(): Optimization { - if (typeof window !== 'undefined' && window.optimization) { - return window.optimization - } +const rootElement = document.getElementById('root') - return createOptimization() +if (!rootElement) { + throw new Error('Missing #root element') } -const optimization = getOptimization() - createRoot(rootElement).render( - + , diff --git a/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts b/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts index e35d1ff8..a5653489 100644 --- a/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts +++ b/platforms/javascript/web-frameworks/react-web/src/hooks/useOptimization.ts @@ -9,7 +9,7 @@ export function useOptimization(): OptimizationWebSdk { if (!context) { throw new Error( 'useOptimization must be used within an OptimizationProvider. ' + - 'Make sure to wrap your component tree with .', + 'Make sure to wrap your component tree with .', ) } diff --git a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx index a312fdd1..aa0eab5f 100644 --- a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx @@ -1,4 +1,5 @@ import Optimization from '@contentful/optimization-web' +import type { ReactElement } from 'react' import { renderToString } from 'react-dom/server' import { LiveUpdatesProvider, @@ -11,14 +12,28 @@ import { usePersonalization, } from './index' -const optimizationInstance = new Optimization({ +const testConfig = { clientId: 'test-client-id', environment: 'main', analytics: { baseUrl: 'http://localhost:8000/insights/' }, personalization: { baseUrl: 'http://localhost:8000/experience/' }, -}) +} + +function cleanupGlobalInstance(): void { + if (typeof window !== 'undefined' && window.optimization) { + window.optimization.destroy() + } +} describe('@contentful/optimization-react-web core providers', () => { + void beforeEach(() => { + cleanupGlobalInstance() + }) + + void afterEach(() => { + cleanupGlobalInstance() + }) + it('exports core API symbols', () => { expect(OptimizationContext).toBeDefined() expect(LiveUpdatesProvider).toBeTypeOf('function') @@ -30,7 +45,7 @@ describe('@contentful/optimization-react-web core providers', () => { expect(useAnalytics).toBeTypeOf('function') }) - it('provides optimization instance via OptimizationProvider', () => { + it('creates optimization instance from config props via OptimizationProvider', () => { let capturedInstance: Optimization | null = null function Probe(): null { @@ -39,60 +54,90 @@ describe('@contentful/optimization-react-web core providers', () => { } renderToString( - + , ) - expect(capturedInstance).toBe(optimizationInstance) + expect(capturedInstance).toBeInstanceOf(Optimization) }) - it('throws actionable error when useOptimization is called outside provider', () => { - function BrokenProbe(): null { - useOptimization() - return null - } - - expect(() => renderToString()).toThrow( - 'useOptimization must be used within an OptimizationProvider', - ) - expect(() => renderToString()).toThrow( - '', - ) - }) - - it('defaults liveUpdates to false in OptimizationRoot', () => { + it('provides optimization and live updates from OptimizationRoot', () => { + let capturedInstance: Optimization | null = null let capturedGlobalLiveUpdates: boolean | null = null function Probe(): null { - capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? false + capturedInstance = useOptimization() + capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? null return null } renderToString( - + , ) - expect(capturedGlobalLiveUpdates).toBe(false) + expect(capturedInstance).toBeInstanceOf(Optimization) + expect(capturedGlobalLiveUpdates).toBe(true) }) - it('passes global live updates through context from OptimizationRoot', () => { - let capturedGlobalLiveUpdates: boolean | null = null + it('throws actionable error when useOptimization is called outside provider', () => { + function BrokenProbe(): null { + useOptimization() + return null + } + + let capturedError: unknown = null + + try { + renderToString() + } catch (error: unknown) { + capturedError = error + } + + expect(capturedError).toBeInstanceOf(Error) + if (!(capturedError instanceof Error)) { + throw new Error('Expected useOptimization to throw an Error') + } + + expect(capturedError.message).toContain( + 'useOptimization must be used within an OptimizationProvider', + ) + expect(capturedError.message).toContain('') + }) + + it('defaults liveUpdates to false in OptimizationRoot', () => { + let capturedGlobalLiveUpdates = false function Probe(): null { - capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? null + capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? false return null } renderToString( - + , ) - expect(capturedGlobalLiveUpdates).toBe(true) + expect(capturedGlobalLiveUpdates).toBe(false) }) it('returns null from useLiveUpdates outside provider', () => { @@ -104,30 +149,9 @@ describe('@contentful/optimization-react-web core providers', () => { } renderToString() - expect(capturedContext).toBeNull() }) - it('provides both optimization instance and live updates via OptimizationRoot', () => { - let capturedInstance: Optimization | null = null - let capturedGlobalLiveUpdates: boolean | null = null - - function Probe(): null { - capturedInstance = useOptimization() - capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? null - return null - } - - renderToString( - - - , - ) - - expect(capturedInstance).toBe(optimizationInstance) - expect(capturedGlobalLiveUpdates).toBe(true) - }) - it('supports live updates fallback semantics for dependent components', () => { const results: boolean[] = [] @@ -138,18 +162,38 @@ describe('@contentful/optimization-react-web core providers', () => { return null } - renderToString( - - - - , - ) + function FirstScenario(): ReactElement { + return ( + + + + + ) + } - renderToString( - - - , - ) + function SecondScenario(): ReactElement { + return ( + + + + ) + } + + renderToString() + cleanupGlobalInstance() + renderToString() expect(results).toEqual([true, false, true]) }) @@ -159,7 +203,6 @@ describe('@contentful/optimization-react-web core providers', () => { const analytics = useAnalytics() expect(personalization.resolveEntry({ id: 'entry-1' })).toEqual({ id: 'entry-1' }) - await expect(analytics.identify('user-1')).resolves.toBeUndefined() await expect(analytics.track({ event: 'view' })).resolves.toBeUndefined() await expect(analytics.reset()).resolves.toBeUndefined() diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx index 1358add7..32e083af 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx @@ -1,17 +1,36 @@ -import type { PropsWithChildren, ReactElement } from 'react' +import Optimization, { + type App, + type CoreStatefulAnalyticsConfig, + type CoreStatefulPersonalizationConfig, + type LogLevels, +} from '@contentful/optimization-web' +import { useRef, type PropsWithChildren, type ReactElement } from 'react' import { OptimizationContext } from '../context/OptimizationContext' -import type { OptimizationWebSdk } from '../types' +// TODO: we need to use the config type that should be export it from the web SDK +type AutoTrackEntryInteractionOptions = Partial> + +// Config-based props (clientId required, rest optional) export interface OptimizationProviderProps extends PropsWithChildren { - readonly instance: OptimizationWebSdk + clientId: string + environment?: string + analytics?: CoreStatefulAnalyticsConfig + personalization?: CoreStatefulPersonalizationConfig + app?: App + autoTrackEntryInteraction?: AutoTrackEntryInteractionOptions + logLevel?: LogLevels } -export function OptimizationProvider({ - children, - instance, -}: OptimizationProviderProps): ReactElement { +export function OptimizationProvider(props: OptimizationProviderProps): ReactElement { + const { children, ...config } = props + const instanceRef = useRef(null) + + instanceRef.current ??= new Optimization(config) + return ( - {children} + + {children} + ) } diff --git a/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx b/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx index 07af1774..b006b369 100644 --- a/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/root/OptimizationRoot.tsx @@ -1,21 +1,22 @@ -import type { PropsWithChildren, ReactElement } from 'react' +import type { ReactElement } from 'react' import { LiveUpdatesProvider } from '../provider/LiveUpdatesProvider' -import { OptimizationProvider } from '../provider/OptimizationProvider' -import type { OptimizationWebSdk } from '../types' +import { + OptimizationProvider, + type OptimizationProviderProps, +} from '../provider/OptimizationProvider' -export interface OptimizationRootProps extends PropsWithChildren { - readonly instance: OptimizationWebSdk +export type OptimizationRootProps = OptimizationProviderProps & { readonly liveUpdates?: boolean } export function OptimizationRoot({ children, - instance, liveUpdates = false, + ...providerProps }: OptimizationRootProps): ReactElement { return ( - + {children} ) From 42698d9c90a6ff48d806cb91757cd59be4d12136 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Tue, 3 Mar 2026 19:10:27 +0100 Subject: [PATCH 10/13] refactor: useLiveUpdates to throw outside provider --- .../web-frameworks/react-web/dev/App.tsx | 6 ++-- .../react-web/src/hooks/useLiveUpdates.ts | 10 ++++-- .../react-web/src/index.test.tsx | 34 +++++++++++++------ 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/dev/App.tsx b/platforms/javascript/web-frameworks/react-web/dev/App.tsx index ebc88a21..4fafcc1e 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/App.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/App.tsx @@ -11,8 +11,8 @@ const sectionTitles = [ ] as const export function App(): ReactElement { + const { globalLiveUpdates } = useLiveUpdates() useOptimization() - const liveUpdates = useLiveUpdates()?.globalLiveUpdates ?? false return (
@@ -24,8 +24,8 @@ export function App(): ReactElement {

SDK Wiring

-

Optimization SDK: READY

-

{`Global liveUpdates: ${liveUpdates ? 'ON' : 'OFF'}`}

+

OptimizationRoot: Active

+

{`Global liveUpdates: ${globalLiveUpdates ? 'ON' : 'OFF'}`}

diff --git a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts index fd7b064b..77219861 100644 --- a/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts +++ b/platforms/javascript/web-frameworks/react-web/src/hooks/useLiveUpdates.ts @@ -1,6 +1,12 @@ import { useContext } from 'react' import { LiveUpdatesContext, type LiveUpdatesContextValue } from '../context/LiveUpdatesContext' -export function useLiveUpdates(): LiveUpdatesContextValue | null { - return useContext(LiveUpdatesContext) +export function useLiveUpdates(): LiveUpdatesContextValue { + const context = useContext(LiveUpdatesContext) + + if (!context) { + throw new Error('useLiveUpdates must be used within a LiveUpdatesProvider') + } + + return context } diff --git a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx index aa0eab5f..21267900 100644 --- a/platforms/javascript/web-frameworks/react-web/src/index.test.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/index.test.tsx @@ -73,7 +73,8 @@ describe('@contentful/optimization-react-web core providers', () => { function Probe(): null { capturedInstance = useOptimization() - capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? null + const { globalLiveUpdates } = useLiveUpdates() + capturedGlobalLiveUpdates = globalLiveUpdates return null } @@ -122,7 +123,8 @@ describe('@contentful/optimization-react-web core providers', () => { let capturedGlobalLiveUpdates = false function Probe(): null { - capturedGlobalLiveUpdates = useLiveUpdates()?.globalLiveUpdates ?? false + const { globalLiveUpdates } = useLiveUpdates() + capturedGlobalLiveUpdates = globalLiveUpdates return null } @@ -140,16 +142,28 @@ describe('@contentful/optimization-react-web core providers', () => { expect(capturedGlobalLiveUpdates).toBe(false) }) - it('returns null from useLiveUpdates outside provider', () => { - let capturedContext: ReturnType = null - - function Probe(): null { - capturedContext = useLiveUpdates() + it('throws actionable error when useLiveUpdates is called outside provider', () => { + function BrokenProbe(): null { + useLiveUpdates() return null } - renderToString() - expect(capturedContext).toBeNull() + let capturedError: unknown = null + + try { + renderToString() + } catch (error: unknown) { + capturedError = error + } + + expect(capturedError).toBeInstanceOf(Error) + if (!(capturedError instanceof Error)) { + throw new Error('Expected useLiveUpdates to throw an Error') + } + + expect(capturedError.message).toContain( + 'useLiveUpdates must be used within a LiveUpdatesProvider', + ) }) it('supports live updates fallback semantics for dependent components', () => { @@ -157,7 +171,7 @@ describe('@contentful/optimization-react-web core providers', () => { function Probe({ liveUpdates }: { liveUpdates?: boolean }): null { const context = useLiveUpdates() - const isLive = liveUpdates ?? context?.globalLiveUpdates ?? false + const isLive = liveUpdates ?? context.globalLiveUpdates results.push(isLive) return null } From cc2576e26e94063b981fe0644138d9dcad1c6d0f Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Tue, 3 Mar 2026 19:28:30 +0100 Subject: [PATCH 11/13] reafactor: config types and update live updates semantics - Use OptimizationConfig for provider props - Change useLiveUpdates to throw outside provider - Remove logLevel from dev config - Update live updates resolution example - Export OptimizationConfig type from web SDK --- .../web-frameworks/react-web/README.md | 4 ++-- .../web-frameworks/react-web/dev/main.tsx | 1 - .../src/provider/OptimizationProvider.tsx | 19 ++----------------- platforms/javascript/web/src/index.ts | 5 +++++ 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/README.md b/platforms/javascript/web-frameworks/react-web/README.md index 01e5e3a0..09cd6782 100644 --- a/platforms/javascript/web-frameworks/react-web/README.md +++ b/platforms/javascript/web-frameworks/react-web/README.md @@ -92,14 +92,14 @@ Available config props: - `useOptimization()` returns the current SDK instance. - `useOptimization()` throws if used outside `OptimizationProvider`. -- `useLiveUpdates()` returns the live updates context or `null` outside `LiveUpdatesProvider`. +- `useLiveUpdates()` throws if used outside `LiveUpdatesProvider`. ### Live Updates Resolution Semantics Consumers should resolve live updates behavior with: ```ts -const isLiveUpdatesEnabled = componentLiveUpdates ?? liveUpdatesContext?.globalLiveUpdates ?? false +const isLiveUpdatesEnabled = componentLiveUpdates ?? liveUpdatesContext.globalLiveUpdates ``` This gives: diff --git a/platforms/javascript/web-frameworks/react-web/dev/main.tsx b/platforms/javascript/web-frameworks/react-web/dev/main.tsx index c4a8f6ab..a29976c2 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/main.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/main.tsx @@ -40,7 +40,6 @@ createRoot(rootElement).render( environment={environment} analytics={{ baseUrl: insightsBaseUrl }} personalization={{ baseUrl: experienceBaseUrl }} - logLevel="debug" liveUpdates={true} > diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx index 32e083af..17873989 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx @@ -1,26 +1,11 @@ import Optimization, { - type App, - type CoreStatefulAnalyticsConfig, - type CoreStatefulPersonalizationConfig, - type LogLevels, + type OptimizationConfig, } from '@contentful/optimization-web' import { useRef, type PropsWithChildren, type ReactElement } from 'react' import { OptimizationContext } from '../context/OptimizationContext' -// TODO: we need to use the config type that should be export it from the web SDK -type AutoTrackEntryInteractionOptions = Partial> - -// Config-based props (clientId required, rest optional) -export interface OptimizationProviderProps extends PropsWithChildren { - clientId: string - environment?: string - analytics?: CoreStatefulAnalyticsConfig - personalization?: CoreStatefulPersonalizationConfig - app?: App - autoTrackEntryInteraction?: AutoTrackEntryInteractionOptions - logLevel?: LogLevels -} +export interface OptimizationProviderProps extends PropsWithChildren {} export function OptimizationProvider(props: OptimizationProviderProps): ReactElement { const { children, ...config } = props diff --git a/platforms/javascript/web/src/index.ts b/platforms/javascript/web/src/index.ts index 6f140d18..2707e57f 100644 --- a/platforms/javascript/web/src/index.ts +++ b/platforms/javascript/web/src/index.ts @@ -14,6 +14,11 @@ import Optimization from './Optimization' export * from '@contentful/optimization-core' export * from './builders/EventBuilder' +export type { + OptimizationTrackingApi, + OptimizationWebConfig, + OptimizationWebConfig as OptimizationConfig, +} from './Optimization' export { CAN_ADD_LISTENERS, ENTRY_SELECTOR, From 5f003193a63070fd99500a6171738883a6f45965 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Tue, 3 Mar 2026 19:32:43 +0100 Subject: [PATCH 12/13] feat: Show Optimization SDK name in dashboard card --- .../javascript/web-frameworks/react-web/dev/App.tsx | 3 ++- .../react-web/src/provider/OptimizationProvider.tsx | 4 +--- platforms/javascript/web/src/index.ts | 10 +++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/dev/App.tsx b/platforms/javascript/web-frameworks/react-web/dev/App.tsx index 4fafcc1e..a3963282 100644 --- a/platforms/javascript/web-frameworks/react-web/dev/App.tsx +++ b/platforms/javascript/web-frameworks/react-web/dev/App.tsx @@ -12,7 +12,7 @@ const sectionTitles = [ export function App(): ReactElement { const { globalLiveUpdates } = useLiveUpdates() - useOptimization() + const optimization = useOptimization() return (
@@ -25,6 +25,7 @@ export function App(): ReactElement {

SDK Wiring

OptimizationRoot: Active

+

{`Optimization SDK: ${optimization.constructor.name}`}

{`Global liveUpdates: ${globalLiveUpdates ? 'ON' : 'OFF'}`}

diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx index 17873989..12ed8cbc 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx @@ -1,6 +1,4 @@ -import Optimization, { - type OptimizationConfig, -} from '@contentful/optimization-web' +import Optimization, { type OptimizationConfig } from '@contentful/optimization-web' import { useRef, type PropsWithChildren, type ReactElement } from 'react' import { OptimizationContext } from '../context/OptimizationContext' diff --git a/platforms/javascript/web/src/index.ts b/platforms/javascript/web/src/index.ts index 2707e57f..c337e10f 100644 --- a/platforms/javascript/web/src/index.ts +++ b/platforms/javascript/web/src/index.ts @@ -14,11 +14,6 @@ import Optimization from './Optimization' export * from '@contentful/optimization-core' export * from './builders/EventBuilder' -export type { - OptimizationTrackingApi, - OptimizationWebConfig, - OptimizationWebConfig as OptimizationConfig, -} from './Optimization' export { CAN_ADD_LISTENERS, ENTRY_SELECTOR, @@ -27,6 +22,11 @@ export { OPTIMIZATION_WEB_SDK_VERSION, } from './constants' export * from './handlers/beaconHandler' +export type { + OptimizationWebConfig as OptimizationConfig, + OptimizationTrackingApi, + OptimizationWebConfig, +} from './Optimization' export * from './storage/LocalStore' export default Optimization From 251c08634e5b0556447b8b74161561ed931bf083 Mon Sep 17 00:00:00 2001 From: Lotfi Arif Date: Wed, 4 Mar 2026 09:46:24 +0100 Subject: [PATCH 13/13] feat: update OptimizationProvider to use OptimizationWebConfig type --- .../react-web/src/provider/OptimizationProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx index 12ed8cbc..e17b8463 100644 --- a/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx +++ b/platforms/javascript/web-frameworks/react-web/src/provider/OptimizationProvider.tsx @@ -1,9 +1,9 @@ -import Optimization, { type OptimizationConfig } from '@contentful/optimization-web' +import Optimization, { type OptimizationWebConfig } from '@contentful/optimization-web' import { useRef, type PropsWithChildren, type ReactElement } from 'react' import { OptimizationContext } from '../context/OptimizationContext' -export interface OptimizationProviderProps extends PropsWithChildren {} +export interface OptimizationProviderProps extends PropsWithChildren {} export function OptimizationProvider(props: OptimizationProviderProps): ReactElement { const { children, ...config } = props