Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 101 additions & 7 deletions platforms/javascript/web-frameworks/react-web/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +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 currently scaffold-only and pre-release.
Core root/provider primitives are implemented.

- Runtime behavior beyond the current placeholder surface is intentionally out of scope for this
phase.
- React Web API semantics and parity coverage with other SDK layers are tracked as planned follow-up
work.
- `OptimizationProvider` + `useOptimization()` context behavior
- `LiveUpdatesProvider` + `useLiveUpdates()` global live updates context
- `OptimizationRoot` provider composition and defaults

## Purpose

Expand Down Expand Up @@ -40,5 +39,100 @@ 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

### Initialization

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 (
<OptimizationRoot
clientId="your-client-id"
environment="main"
analytics={{ baseUrl: 'https://insights.contentful.com/' }}
personalization={{ baseUrl: 'https://experience.contentful.com/' }}
liveUpdates={true}
>
<YourApp />
</OptimizationRoot>
)
}
```

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

### Hooks

- `useOptimization()` returns the current SDK instance.
- `useOptimization()` throws if used outside `OptimizationProvider`.
- `useLiveUpdates()` throws if used outside `LiveUpdatesProvider`.

### Live Updates Resolution Semantics

Consumers should resolve live updates behavior with:

```ts
const isLiveUpdatesEnabled = componentLiveUpdates ?? liveUpdatesContext.globalLiveUpdates
```

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(
<OptimizationRoot
clientId="test-client-id"
environment="main"
analytics={{ baseUrl: 'http://localhost:8000/insights/' }}
personalization={{ baseUrl: 'http://localhost:8000/experience/' }}
>
<ComponentUnderTest />
</OptimizationRoot>,
)
```
15 changes: 14 additions & 1 deletion platforms/javascript/web-frameworks/react-web/dev/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactElement } from 'react'
import { useLiveUpdates, useOptimization } from '../src'

const sectionTitles = [
'Consent',
Expand All @@ -10,13 +11,25 @@ const sectionTitles = [
] as const

export function App(): ReactElement {
const { globalLiveUpdates } = useLiveUpdates()
const optimization = useOptimization()

return (
<main className="dashboard">
<header className="dashboard__header">
<h1>@contentful/optimization-react-web</h1>
<p>Scaffold dashboard for React Web SDK development.</p>
<p>Minimal live integration with OptimizationRoot config props.</p>
</header>

<section className="dashboard__grid" style={{ marginTop: '1rem', marginBottom: '0.5rem' }}>
<article className="dashboard__card">
<h2>SDK Wiring</h2>
<p>OptimizationRoot: Active</p>
<p>{`Optimization SDK: ${optimization.constructor.name}`}</p>
<p>{`Global liveUpdates: ${globalLiveUpdates ? 'ON' : 'OFF'}`}</p>
</article>
</section>

<section className="dashboard__grid">
{sectionTitles.map((title) => (
<article className="dashboard__card" key={title}>
Expand Down
33 changes: 32 additions & 1 deletion platforms/javascript/web-frameworks/react-web/dev/main.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { OptimizationRoot } from '../src'

import { App } from './App'
import './styles.css'

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

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
}

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

const rootElement = document.getElementById('root')

if (!rootElement) {
Expand All @@ -12,6 +35,14 @@ if (!rootElement) {

createRoot(rootElement).render(
<StrictMode>
<App />
<OptimizationRoot
clientId={clientId}
environment={environment}
analytics={{ baseUrl: insightsBaseUrl }}
personalization={{ baseUrl: experienceBaseUrl }}
liveUpdates={true}
>
<App />
</OptimizationRoot>
</StrictMode>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createContext } from 'react'

export interface LiveUpdatesContextValue {
readonly globalLiveUpdates: boolean
}

export const LiveUpdatesContext = createContext<LiveUpdatesContextValue | null>(null)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createContext } from 'react'

import type { OptimizationWebSdk } from '../types'

export interface OptimizationContextValue {
readonly instance: OptimizationWebSdk
}

export const OptimizationContext = createContext<OptimizationContextValue | null>(null)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useContext } from 'react'
import { LiveUpdatesContext, type LiveUpdatesContextValue } from '../context/LiveUpdatesContext'

export function useLiveUpdates(): LiveUpdatesContextValue {
const context = useContext(LiveUpdatesContext)

if (!context) {
throw new Error('useLiveUpdates must be used within a LiveUpdatesProvider')
}

return context
}
Original file line number Diff line number Diff line change
@@ -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 <OptimizationRoot clientId="your-client-id">.',
)
}

return context.instance
}
31 changes: 0 additions & 31 deletions platforms/javascript/web-frameworks/react-web/src/index.test.ts

This file was deleted.

Loading