diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..641b1e4 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,720 @@ +# Migrating to React SDK v4 + +This guide covers breaking changes and how to update your code when upgrading from React SDK v3 to v4. + +- [Architecture changes](#architecture-changes) +- [Installation](#installation) +- [Underlying JS SDK changes (v5 to v6)](#underlying-js-sdk-changes-v5-to-v6) +- [Client creation](#client-creation) +- [Provider](#provider) +- [Hooks](#hooks) + - [useDecision to useDecide](#usedecision-to-usedecide) + - [useExperiment (removed)](#useexperiment-removed) + - [useFeature (removed)](#usefeature-removed) + - [useTrackEvent (removed)](#usetrackevent-removed) + - [New hooks](#new-hooks) +- [Accessing the client directly](#accessing-the-client-directly) +- [Removed APIs](#removed-apis) +- [Event tracking](#event-tracking) +- [Forced decisions](#forced-decisions) +- [Logger](#logger) +- [Server-side rendering](#server-side-rendering) +- [React Server Components](#react-server-components) +- [TypeScript changes](#typescript-changes) + +--- + +## Architecture changes + +v4 is a ground-up rewrite with a fundamentally different architecture: + +| Aspect | v3 | v4 | +|--------|----|----| +| Underlying JS SDK | v5 (`@optimizely/optimizely-sdk`) | v6 (`@optimizely/optimizely-sdk`) | +| Client model | Stateful `ReactSDKClient` wrapper (user bound to client) | Thin wrapper over the JS SDK `Client` (user managed by Provider) | +| Readiness model | `[value, clientReady, didTimeout]` tuples | `{ decision, isLoading, error }` discriminated unions | +| Datafile updates | `autoUpdate` option per hook | Automatic via SDK polling; hooks re-evaluate on config changes | +| User overrides | Per-hook `overrideUserId` / `overrideAttributes` | Removed; use separate `` instances | +| Components | `OptimizelyExperiment`, `OptimizelyFeature`, `OptimizelyVariation` | Removed; use hooks | +| HOC | `withOptimizely` | Removed; use hooks | + +--- + +## Installation + +```bash +npm install @optimizely/react-sdk@4 +``` + +### Breaking environment changes + +| | v3 | v4 | +|--|----|----| +| Module format | ESM + CommonJS | **ESM only** (`import` / `default` — no `require` entry point) | +| Node.js | >=14.0.0 | **>=18.0.0** | +| React peer dependency | >=16.8.0 | >=16.8.0 (unchanged) | + +If your project uses CommonJS (`require()`), you will need to switch to ESM imports or configure your bundler to handle ESM dependencies. + +--- + +## Underlying JS SDK changes (v5 to v6) + +React SDK v4 upgrades the underlying `@optimizely/optimizely-sdk` from v5 to v6. This brings several behavioral changes that affect React SDK usage. For full details, see the [JavaScript SDK Migration Guide](https://github.com/optimizely/javascript-sdk/blob/master/MIGRATION.md). + +### Modular architecture + +The monolithic `createInstance` config is now split into dedicated factory functions. Options like `sdkKey`, `datafile`, event batching, ODP, and logging are no longer top-level — each has its own factory. See [Client creation](#client-creation) for details. + +### Opt-in components + +Several features that were enabled by default in v5 are now opt-in in v6: + +| Component | v5 (v3 React SDK) | v6 (v4 React SDK) | +|-----------|-------------------|-------------------| +| Event processing | Enabled by default (batch processor) | **Opt-in** — pass `eventProcessor` to `createInstance`, otherwise no events are dispatched | +| ODP | Enabled by default (configured via `odpOptions`) | **Opt-in** — pass `odpManager` to `createInstance`, otherwise ODP is disabled | +| VUID tracking | Enabled by default | **Opt-in** — pass `vuidManager` to `createInstance` | +| Logging | Enabled by default | **Opt-in** — pass `logger` to `createInstance`, otherwise logging is disabled | + +### `onReady` behavior + +In v5, `onReady()` always fulfilled with `{ success: boolean, reason?: string }`. In v6, `onReady()` fulfills when the client is ready and rejects on failure: + +```jsx +// v3 +optimizely.onReady().then(({ success, reason }) => { + if (success) { /* ready */ } + else { console.log(reason); } +}); + +// v4 +optimizely.onReady() + .then(() => { /* ready */ }) + .catch((err) => { console.error(err); }); +``` + +> **Note:** When using hooks (`useDecide`, etc.), you don't call `onReady` directly — the Provider and hooks handle readiness internally. This change primarily affects direct client usage in server components or outside the Provider. + +### `createInstance` error handling + +In v3, `createInstance` returned `null` on invalid config. In v4, it throws an error. Wrap the call in a try/catch if you need to handle invalid configurations. + +--- + +## Client creation + +### v3 + +```jsx +import { createInstance } from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + sdkKey: 'your-sdk-key', + datafile: window.optimizelyDatafile, + // v3-specific options + eventBatchSize: 10, + eventFlushInterval: 2000, +}); +``` + +`createInstance` returned a `ReactSDKClient` — a custom wrapper around the JS SDK with user management, readiness tracking, and React-specific methods. + +### v4 + +In v4, the `Config` type is modular. Options like `sdkKey`, `datafile`, and event batching are no longer top-level — they are configured through dedicated factory functions. The only required field is `projectConfigManager`. + +```jsx +import { + createInstance, + createPollingProjectConfigManager, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-sdk-key', + datafile: window.optimizelyDatafile, // optional: use as initial datafile while polling for updates + autoUpdate: true, + }), +}); +``` + +`createInstance` from `@optimizely/react-sdk` returns a JS SDK v6 `Client` augmented with React-specific metadata. The client no longer holds user state — that responsibility moves to ``. + +> **Important:** You must use `createInstance` from `@optimizely/react-sdk`, not from `@optimizely/optimizely-sdk`. A client created directly from the JS SDK will not work correctly with `` and hooks. + +**Key differences:** +- `sdkKey` and `datafile` are passed to a config manager factory, not to `createInstance` directly. +- You can no longer call `optimizely.setUser()` or other v3-specific wrapper methods on the returned client. Use hooks or the JS SDK client API instead. + +### Config manager factories + +| Factory | Use case | +|---------|----------| +| `createPollingProjectConfigManager()` | Fetches and polls for datafile updates. `sdkKey` is required. | +| `createStaticProjectConfigManager()` | Uses a fixed datafile with no polling. | + +### Event processor factories + +| Factory | Use case | +|---------|----------| +| `createBatchEventProcessor()` | Batches events before dispatching. | +| `createForwardingEventProcessor()` | Forwards each event immediately. | + +### Other configurable modules + +| Factory | Use case | +|---------|----------| +| `createOdpManager()` | Enables ODP integration (audience segments, events). | +| `createVuidManager()` | Enables visitor UID tracking. | +| `createErrorNotifier()` | Configures error notification. | +| `createLogger({ logLevel })` | Creates a logger instance (see [Logger](#logger)). | + +### Full example + +```jsx +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + createOdpManager, + createLogger, + DEBUG, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-sdk-key', + }), + eventProcessor: createBatchEventProcessor({ + batchSize: 10, + flushInterval: 2000, + }), + odpManager: createOdpManager(), + logger: createLogger({ logLevel: DEBUG }), +}); +``` + +--- + +## Provider + +### v3 + +```jsx +import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; + +const optimizely = createInstance({ sdkKey: 'your-sdk-key' }); + +// v3 Provider accepted ReactSDKClient as `optimizely` prop + + + +``` + +v3 also supported deprecated `userId` and `userAttributes` props, and a `Promise` for async user resolution. + +### v4 + +```jsx +import { OptimizelyProvider, createInstance, createPollingProjectConfigManager } from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ sdkKey: 'your-sdk-key' }), +}); + +// v4 Provider uses `client` prop (not `optimizely`) + + + +``` + +### Prop changes + +| v3 Prop | v4 Prop | Notes | +|---------|---------|-------| +| `optimizely` | `client` | Renamed. Now accepts a JS SDK `Client` (from `createInstance`). | +| `user` | `user` | Same shape `{ id, attributes }`. **No longer accepts a `Promise`**. | +| `timeout` | `timeout` | Default changed from `5000` ms to `30000` ms. | +| `isServerSide` | _(removed)_ | No longer needed. v4 hooks return decisions synchronously whenever both user context and config are available, regardless of environment. | +| `userId` | _(removed)_ | Deprecated in v3, removed in v4. Use `user` instead. | +| `userAttributes` | _(removed)_ | Deprecated in v3, removed in v4. Use `user` instead. | +| _(new)_ | `skipSegments` | Skips ODP segment fetching. Default `false`. | +| `qualifiedSegments` | `qualifiedSegments` | Pre-fetched ODP segments for the user. Same behavior in both versions. | + +### Async user loading + +v3 allowed passing a `Promise` to the `user` prop. In v4, resolve the user before rendering the Provider: + +```jsx +// v3 +const userPromise = fetchUser(); + + +// v4 — resolve the user first +function AppWrapper() { + const [user, setUser] = useState(null); + + useEffect(() => { + fetchUser().then(setUser); + }, []); + + if (!user) return ; + + return ( + + + + ); +} +``` + +--- + +## Hooks + +### `useDecision` to `useDecide` + +The primary decision hook has been renamed and its signature changed. + +#### v3 + +```jsx +import { useDecision } from '@optimizely/react-sdk'; + +const [decision, clientReady, didTimeout] = useDecision( + 'flag-key', + { autoUpdate: true, timeout: 500, decideOptions: [OptimizelyDecideOption.INCLUDE_REASONS] }, + { overrideUserId: 'other-user', overrideAttributes: { plan: 'gold' } } +); + +if (!clientReady) return ; +if (decision.enabled) return ; +``` + +#### v4 + +```jsx +import { useDecide } from '@optimizely/react-sdk'; + +const { decision, isLoading, error } = useDecide( + 'flag-key', + { decideOptions: [OptimizelyDecideOption.INCLUDE_REASONS] } +); + +if (isLoading) return ; +if (error) return ; +if (decision.enabled) return ; +``` + +**What changed:** + +| Aspect | v3 `useDecision` | v4 `useDecide` | +|--------|-------------------|----------------| +| Import name | `useDecision` | `useDecide` | +| Return type | `[decision, clientReady, didTimeout]` tuple | `{ decision, isLoading, error }` object | +| `autoUpdate` option | Per-hook opt-in | Removed; updates are automatic via SDK polling | +| `timeout` option | Per-hook override | Removed; set on `` only | +| `overrideUserId` | Third argument | Removed | +| `overrideAttributes` | Third argument | Removed | +| Error handling | Check `decision` for failed state | Explicit `error` property | +| Loading state | `!clientReady` | `isLoading: true` | +| Decision type when loading | Failed `OptimizelyDecision` object | `null` | + +### `useExperiment` (removed) + +`useExperiment` is removed in v4 with no hook replacement. For programmatic access, `client.activate()` is still available on the client. If hook-level reactivity for experiments is needed, consider staying on v3 for those components. + +### `useFeature` (removed) + +`useFeature` is removed in v4 with no hook replacement. For programmatic access, `client.isFeatureEnabled()` is still available on the client. If hook-level reactivity for feature flags is needed, consider staying on v3 for those components. + +### `useTrackEvent` (removed) + +`useTrackEvent` is removed in v4. Use `useOptimizelyUserContext` to track events: + +```jsx +const { userContext } = useOptimizelyUserContext(); + +const handleClick = () => { + if (userContext) { + userContext.trackEvent('my-event', { revenue: 100 }); + } +}; +``` + +### New hooks + +v4 introduces several new hooks: + +| Hook | Description | +|------|-------------| +| `useDecide(flagKey, config?)` | Single flag decision (replaces `useDecision`) | +| `useDecideForKeys(flagKeys[], config?)` | Batch decisions for multiple flag keys | +| `useDecideAll(config?)` | Decisions for all active flags | +| `useDecideAsync(flagKey, config?)` | Async variant of `useDecide` | +| `useDecideForKeysAsync(flagKeys[], config?)` | Async variant of `useDecideForKeys` | +| `useDecideAllAsync(config?)` | Async variant of `useDecideAll` | +| `useOptimizelyClient()` | Returns the Optimizely `Client` instance from context | +| `useOptimizelyUserContext()` | Returns `{ userContext, isLoading, error }` | + +#### Multi-flag decisions + +```jsx +import { useDecideForKeys } from '@optimizely/react-sdk'; + +const { decisions, isLoading, error } = useDecideForKeys(['flag-a', 'flag-b']); + +if (!isLoading) { + const flagA = decisions['flag-a']; + const flagB = decisions['flag-b']; +} +``` + +#### Async decisions + +```jsx +import { useDecideAsync } from '@optimizely/react-sdk'; + +const { decision, isLoading, error } = useDecideAsync('flag-key'); +``` + +Async hooks call the underlying async SDK methods (`decideAsync`, `decideForKeysAsync`, `decideAllAsync`). Use these when your setup involves asynchronous operations such as CMAB (Contextual Multi-Armed Bandit) decisions or async User Profile Service lookups. + +--- + +## Accessing the client directly + +### v3 — `withOptimizely` HOC or `OptimizelyContext` + +```jsx +import { withOptimizely } from '@optimizely/react-sdk'; + +class MyComponent extends React.Component { + render() { + const { optimizely } = this.props; + const decision = optimizely.decide('flag-key'); + return
{decision.enabled ? 'On' : 'Off'}
; + } +} + +export default withOptimizely(MyComponent); +``` + +### v4 — `useOptimizelyClient` hook + +```jsx +import { useOptimizelyClient } from '@optimizely/react-sdk'; + +function MyComponent() { + const client = useOptimizelyClient(); +} +``` + +--- + +## Removed APIs + +The following v3 exports are removed in v4: + +### Components +- `OptimizelyExperiment` — Removed along with its underlying `useExperiment` hook. +- `OptimizelyFeature` — Removed along with its underlying `useFeature` hook. +- `OptimizelyVariation` — Removed as it was only used as a child of `OptimizelyExperiment`. + +### HOC +- `withOptimizely` — Use `useOptimizelyClient` hook instead. + +### Hooks +- `useExperiment` — Removed with no hook replacement. Use `client.activate()` for programmatic access. +- `useFeature` — Removed with no hook replacement. Use `client.isFeatureEnabled()` for programmatic access. +- `useDecision` — Renamed to `useDecide` with a new return type. +- `useTrackEvent` — Use `useOptimizelyUserContext` instead. + +### Client methods + +Methods like `activate()`, `getVariation()`, `isFeatureEnabled()`, `getFeatureVariables()`, `getEnabledFeatures()`, `setForcedVariation()`, `getForcedVariation()`, and `track()` are still available on the client. As user is now decoupled from the client in v4, `userId` is a required parameter on all these methods. + +The following v3-specific wrapper methods are removed: +- `setUser()` / `onUserUpdate()` — User is managed by `` props. + +### Context exports +- `OptimizelyContext` — Use `useOptimizelyClient` or `useOptimizelyUserContext` hooks. +- `OptimizelyContextConsumer` — Use hooks instead of the context consumer. +- `OptimizelyContextProvider` — Internal; use `` directly. + +### Others +- `logOnlyEventDispatcher` — To disable event dispatching, simply don't pass an `eventProcessor` to `createInstance` (event processing is opt-in in v4). +- `setLogger` / `setLogLevel` — Replaced by `createLogger()` factory (see [Logger](#logger)). +- `logging` — No longer needed; use `createLogger()`. +- `errorHandler` — No longer needed; use `createErrorNotifier()`. +- `enums` — Removed. + +--- + +## Event tracking + +### v3 + +```jsx +// Via useTrackEvent +const [track] = useTrackEvent(); +track('purchase', undefined, undefined, { revenue: 4200 }); + +// Via withOptimizely +const { optimizely } = this.props; +optimizely.track('purchase'); +``` + +### v4 + +```jsx +const { userContext } = useOptimizelyUserContext(); +userContext?.trackEvent('purchase', { revenue: 4200 }); +``` + +--- + +## Forced decisions + +### v3 + +```jsx +const { optimizely } = this.props; // via withOptimizely +optimizely.setForcedDecision( + { flagKey: 'flag-1', ruleKey: 'rule-1' }, + { variationKey: 'variation-a' } +); +``` + +### v4 + +Forced decisions are set on the `userContext` object: + +```jsx +const { userContext } = useOptimizelyUserContext(); + +// Set a forced decision +userContext?.setForcedDecision( + { flagKey: 'flag-1', ruleKey: 'rule-1' }, + { variationKey: 'variation-a' } +); + +// Remove a forced decision +userContext?.removeForcedDecision({ flagKey: 'flag-1', ruleKey: 'rule-1' }); + +// Remove all forced decisions +userContext?.removeAllForcedDecisions(); +``` + +Hooks that use the affected flag key automatically re-render when forced decisions change. + +--- + +## Logger + +### v3 + +```jsx +import { createInstance, setLogLevel } from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + sdkKey: 'your-sdk-key', + logLevel: 'debug', +}); +``` + +### v4 + +```jsx +import { + createInstance, + createPollingProjectConfigManager, + createLogger, + DEBUG, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ sdkKey: 'your-sdk-key' }), + logger: createLogger({ logLevel: DEBUG }), +}); +``` + +Logging is **disabled by default** in v4. You must pass a `logger` to `createInstance` to enable it. The `createLogger` function accepts a `logLevel` option, and the log level constants (`DEBUG`, `INFO`, `WARN`, `ERROR`) are exported for convenience. + +--- + +## Server-side rendering + +### v3 + +```jsx + + + +``` + +### v4 + +The `isServerSide` prop is removed. Instead, configure the client for SSR use: + +```jsx +import { + createInstance, + createStaticProjectConfigManager, + createPollingProjectConfigManager, + OptimizelyProvider, + OptimizelyDecideOption, +} from '@optimizely/react-sdk'; + +const isServerSide = typeof window === 'undefined'; + +const optimizely = createInstance({ + projectConfigManager: isServerSide + ? createStaticProjectConfigManager({ datafile }) // pre-fetched datafile, no polling + : createPollingProjectConfigManager({ + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY, + datafile, // optional: use as initial datafile while polling + }), + defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], +}); + + + + +``` + +### ODP segments during SSR + +ODP audience segments require async I/O, which is not available during synchronous server rendering. If your audience conditions depend on ODP segments, you can pre-fetch them server-side using `getQualifiedSegments` and pass them to the Provider. + +`getQualifiedSegments` is available in both v3 and v4, but the return type has changed: + +```ts +// v3 +import { getQualifiedSegments } from '@optimizely/react-sdk'; + +const segments = await getQualifiedSegments(userId, datafile); +// segments: string[] | null + +// v4 +import { getQualifiedSegments } from '@optimizely/react-sdk'; + +const { segments, error } = await getQualifiedSegments(userId, datafile); +// returns QualifiedSegmentsResult { segments: string[], error: Error | null } +``` + +```jsx +// v3 + + + + +// v4 + + + +``` + +- `qualifiedSegments` — Pass pre-fetched segments so the Provider can create the user context synchronously with segments already set. Available in both v3 and v4. +- `skipSegments` — *(New in v4)* When `true`, skips the Provider's background ODP segment fetch. Use this on the server to avoid unnecessary async work. + +--- + +## React Server Components + +v4 provides a server-safe entry point via the `react-server` export condition in `package.json`. Frameworks that support this condition (e.g., Next.js App Router) automatically resolve `@optimizely/react-sdk` to the server entry point when importing from a Server Component. This entry point excludes hooks and Provider (which use client-only React APIs), so it is safe to import in server contexts. + +```tsx +import { createInstance, createStaticProjectConfigManager } from '@optimizely/react-sdk'; + +export default async function ServerComponent() { + const client = createInstance({ + projectConfigManager: createStaticProjectConfigManager({ datafile }), + }); + + await client.onReady(); + + const userContext = client.createUserContext('user-123'); + const decision = userContext.decide('flag-key'); + + client.close(); + + return decision.enabled ? : ; +} +``` + +--- + +## TypeScript changes + +### Renamed / moved types + +| v3 | v4 | +|----|----| +| `ReactSDKClient` | `Client` | + +### New types + +| Type | Description | +|------|-------------| +| `UseDecideConfig` | Config object for `useDecide` — `{ decideOptions?: OptimizelyDecideOption[] }` | +| `UseDecideResult` | Return type of `useDecide` — discriminated union of loading/error/success | +| `UseDecideMultiResult` | Return type of `useDecideForKeys` / `useDecideAll` | +| `OptimizelyProviderProps` | Props for `` | +| `UserInfo` | `{ id?: string; attributes?: UserAttributes }` | +| `QualifiedSegmentsResult` | Return type of `getQualifiedSegments` — `{ segments: string[], error: Error \| null }` (replaces `string[] \| null` from v3) | + +### Return type changes + +v3 hooks returned positional tuples. v4 hooks return discriminated union objects: + +```ts +// v3 +type UseDecisionReturn = [OptimizelyDecision, boolean, boolean]; + +// v4 +type UseDecideResult = + | { isLoading: true; error: null; decision: null } + | { isLoading: false; error: Error; decision: null } + | { isLoading: false; error: null; decision: OptimizelyDecision }; +``` + +This pattern enables exhaustive narrowing: + +```ts +const result = useDecide('flag'); + +if (result.isLoading) { + // result.decision is null, result.error is null +} +if (result.error) { + // result.decision is null, result.isLoading is false +} +// result.decision is OptimizelyDecision +``` diff --git a/README.md b/README.md index 6df1eb2..9947861 100644 --- a/README.md +++ b/README.md @@ -1,614 +1,784 @@ -# Optimizely React SDK - -This repository houses the React SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). - -Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/introduction). - -Optimizely Rollouts is [free feature flags](https://www.optimizely.com/free-feature-flagging/) for development teams. You can easily roll out and roll back features in any application without code deploys, mitigating risk for every feature on your roadmap. - -## Get Started - -Refer to the [React SDK's developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-react-sdk) for detailed instructions on getting started with using the SDK. - -For React Native, review the [React Native developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-react-native-sdk). - -### Features - -- Automatic datafile downloading -- User ID + attributes memoization -- Render blocking until datafile is ready via a React API -- Optimizely timeout (only block rendering up to the number of milliseconds you specify) -- Library of React components and hooks to use with [feature flags](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/create-feature-flags) - -### Compatibility - -The React SDK is compatible with `React 16.8.0 +` - -### Example - -```jsx -import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/react-sdk'; - -const optimizelyClient = createInstance({ - sdkKey: 'your-optimizely-sdk-key', -}); - -function MyComponent() { - const [decision] = useDecision('sort-algorithm'); - return ( - - - {decision.variationKey === 'relevant_first' && } - {decision.variationKey === 'recent_first' && } - - ); -} - -class App extends React.Component { - render() { - return ( - - - - ); - } -} -``` - -### Install the SDK - -``` -npm install @optimizely/react-sdk -``` - -For **React Native**, installation instruction is bit different. Check out the - -- [Official Installation guide](https://docs.developers.optimizely.com/feature-experimentation/docs/install-sdk-reactnative) -- [Expo React Native Sample App](https://github.com/optimizely/expo-react-native-sdk-sample) - -## Use the React SDK - -### Initialization - -## `createInstance` - -The `ReactSDKClient` client created via `createInstance` is the programmatic API to evaluating features and experiments and tracking events. The `ReactSDKClient` is what powers the rest of the ReactSDK internally. - -_arguments_ - -- `config : object` Object with SDK configuration parameters. This has the same format as the object passed to the `createInstance` method of the core `@optimizely/javascript-sdk` module. For details on this object, see the following pages from the developer docs: - - [Instantiate](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/initialize-sdk-react) - - [JavaScript: Client-side Datafile Management](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-client-side-implementation) - -_returns_ - -- A `ReactSDKClient` instance. - -```jsx -import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; - -const optimizely = createInstance({ - datafile: window.optimizelyDatafile, -}); -``` - -## `` - -Required at the root level. Leverages React’s `Context` API to allow access to the `ReactSDKClient` to the `useDecision` hook. - -_props_ - -| Prop | Type | Required | Description | -| --- | --- | --- | --- | -| `optimizely` | `ReactSDKClient` | Yes | Instance created from `createInstance` | -| `user` | `{ id: string; attributes?: { [key: string]: any } }` \| `Promise` | No | User info object — `id` and `attributes` will be passed to the SDK for every feature flag, A/B test, or `track` call. Can also be a `Promise` for the same kind of object. | -| `timeout` | `number` | No | The amount of time for `useDecision` to return `null` flag Decision while waiting for the SDK instance to become ready, before resolving. | -| `isServerSide` | `boolean` | No | Must pass `true` for server side rendering. | -| `userId` | `string` | No | **Deprecated, prefer `user` instead.** Another way to provide user id. The `user` prop takes precedence when both are provided. | -| `userAttributes` | `object` | No | **Deprecated, prefer `user` instead.** Another way to provide user attributes. The `user` prop takes precedence when both are provided. | -| `qualifiedSegments` | `string[] \| null` | No | Pre-fetched ODP audience segments for the user. Useful during SSR where async segment fetching is unavailable. Use [`getQualifiedSegments`](#getqualifiedsegments) to obtain these segments server-side. | - -### Readiness - -Before rendering real content, both the datafile and the user must be available to the SDK. - -#### Load the datafile synchronously - -Synchronous loading is the preferred method to ensure that Optimizely is always ready and doesn't add any delay or asynchronous complexity to your application. When initializing with both the SDK key and datafile, the SDK will use the given datafile to start, then download the latest version of the datafile in the background. - -```jsx -import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; - -const optimizelyClient = createInstance({ - datafile: window.optimizelyDatafile, - sdkKey: 'your-optimizely-sdk-key', // Optimizely environment key -}); - -class AppWrapper extends React.Component { - render() { - return ( - - - - ); - } -} -``` - -#### Load the datafile asynchronously - -If you don't have the datafile downloaded, the `ReactSDKClient` can fetch the datafile for you. However, instead of waiting for the datafile to fetch before you render your app, you can immediately render your app and provide a `timeout` option to ``. The `useDecision` hook returns `isClientReady` and `didTimeout`. You can use these to block rendering of component until the datafile loads or the timeout is over. - -```jsx -import { OptimizelyProvider, createInstance, useDecision } from '@optimizely/react-sdk'; - -const optimizelyClient = createInstance({ - sdkKey: 'your-optimizely-sdk-key', // Optimizely environment key -}); - -function MyComponent() { - const [decision, isClientReady, didTimeout] = useDecision('the-flag'); - return ( - - {isClientReady &&
The Component
} - {didTimeout &&
Default Component
} - {/* If client is not ready and time out has not occured yet, do not render anything */} -
- ); -} - -class App extends React.Component { - render() { - return ( - - - - ); - } -} -``` - -#### Set user asynchronously - -If user information is synchronously available, it can be provided as the `user` object prop, as in prior examples. But, if user information must be fetched asynchronously, the `user` prop can be a `Promise` for a `user` object with the same properties (`id` and `attributes`): - -```jsx -import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; -import { fetchUser } from './user'; - -const optimizely = createInstance({ - datafile: window.optimizelyDatafile, -}); - -const userPromise = fetchUser(); // fetchUser returns a Promise for an object with { id, attributes } - -class AppWrapper extends React.Component { - render() { - return ( - - - - ); - } -} -``` - -## `useDecision` Hook - -A [React Hook](https://react.dev/learn/state-a-components-memory#meet-your-first-hook) to retrieve the decision result for a flag key, optionally auto updating that decision based on underlying user or datafile changes. - -_arguments_ - -- `flagKey : string` The key of the feature flag. -- `options : Object` - - `autoUpdate : boolean` (optional) If true, this hook will update the flag decision in response to datafile or user changes. Default: `false`. - - `timeout : number` (optional) Client timeout as described in the `OptimizelyProvider` section. Overrides any timeout set on the ancestor `OptimizelyProvider`. - - `decideOption: OptimizelyDecideOption[]` (optional) Array of OptimizelyDecideOption enums. -- `overrides : Object` - - `overrideUserId : string` (optional) Override the userId to be used to obtain the decision result for this hook. - - `overrideAttributes : optimizely.UserAttributes` (optional) Override the user attributes to be used to obtain the decision result for this hook. - -_returns_ - -- `Array` of: - - - `decision : OptimizelyDecision` - Decision result for the flag key. - - `clientReady : boolean` - Whether or not the underlying `ReactSDKClient` instance is ready or not. - - `didTimeout : boolean` - Whether or not the underlying `ReactSDKClient` became ready within the allowed `timeout` range. - - _Note: `clientReady` can be true even if `didTimeout` is also true. This indicates that the client became ready *after* the timeout period._ - -### Render something if flag is enabled - -```jsx -import { useEffect } from 'react'; -import { useDecision } from '@optimizely/react-sdk'; - -function LoginComponent() { - const [decision, clientReady] = useDecision( - 'login-flag', - { autoUpdate: true }, - { - /* (Optional) User overrides */ - } - ); - useEffect(() => { - document.title = decision.enabled ? 'login-new' : 'login-default'; - }, [decision.enabled]); - - return ( -

- Click to login -

- ); -} -``` - -## `withOptimizely` - -Any component under the `` can access the Optimizely `ReactSDKClient` via the higher-order component (HoC) `withOptimizely`. - -_arguments_ - -- `Component : React.Component` Component which will be enhanced with the following props: - - `optimizely : ReactSDKClient` The client object which was passed to the `OptimizelyProvider` - - `optimizelyReadyTimeout : number | undefined` The timeout which was passed to the `OptimizelyProvider` - - `isServerSide : boolean` Value that was passed to the `OptimizelyProvider` - -_returns_ - -- A wrapped component with additional props as described above - -### Example - -```jsx -import { withOptimizely } from '@optimizely/react-sdk'; - -class MyComp extends React.Component { - constructor(props) { - super(props); - const { optimizely } = this.props; - const decision = optimizely.decide('feat1'); - - this.state = { - decision.enabled, - decision.variables, - }; - } - - render() {} -} - -const WrappedMyComponent = withOptimizely(MyComp); -``` - -**_Note:_** The `optimizely` client object provided via `withOptimizely` is automatically associated with the `user` prop passed to the ancestor `OptimizelyProvider` - the `id` and `attributes` from that `user` object will be automatically forwarded to all appropriate SDK method calls. So, there is no need to pass the `userId` or `attributes` arguments when calling methods of the `optimizely` client object, unless you wish to use _different_ `userId` or `attributes` than those given to `OptimizelyProvider`. - -## `useContext` - -Any component under the `` can access the Optimizely `ReactSDKClient` via the `OptimizelyContext` with `useContext`. - -_arguments_ - -- `OptimizelyContext : React.Context` The Optimizely context initialized in a parent component (or App). - -_returns_ - -- Wrapped object: - - `optimizely : ReactSDKClient` The client object which was passed to the `OptimizelyProvider` - - `isServerSide : boolean` Value that was passed to the `OptimizelyProvider` - - `timeout : number | undefined` The timeout which was passed to the `OptimizelyProvider` - -### Example - -```jsx -import React, { useContext } from 'react'; -import { OptimizelyContext } from '@optimizely/react-sdk'; - -function MyComponent() { - const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext); - const decision = optimizely.decide('my-feature'); - const onClick = () => { - optimizely.track('signup-clicked'); - // rest of your click handling code - }; - return ( - <> - {decision.enabled &&

My feature is enabled

} - {!decision.enabled &&

My feature is disabled

} - {decision.variationKey === 'control-variation' &&

Current Variation

} - {decision.variationKey === 'experimental-variation' &&

Better Variation

} - - - ); -} -``` - -### Tracking - -Use the built-in `useTrackEvent` hook to access the `track` method of optimizely instance - -```jsx -import { useTrackEvent } from '@optimizely/react-sdk'; - -function SignupButton() { - const [track, clientReady, didTimeout] = useTrackEvent(); - - const handleClick = () => { - if (clientReady) { - track('signup-clicked'); - } - }; - - return ; -} -``` - -Or you can use the `withOptimizely` HoC. - -```jsx -import { withOptimizely } from '@optimizely/react-sdk'; - -class SignupButton extends React.Component { - onClick = () => { - const { optimizely } = this.props; - optimizely.track('signup-clicked'); - // rest of click handler - }; - - render() { - ; - } -} - -const WrappedSignupButton = withOptimizely(SignupButton); -``` - -**_Note:_** As mentioned above, the `optimizely` client object provided via `withOptimizely` is automatically associated with the `user` prop passed to the ancestor `OptimizelyProvider.` There is no need to pass `userId` or `attributes` arguments when calling `track`, unless you wish to use _different_ `userId` or `attributes` than those given to `OptimizelyProvider`. - -## `ReactSDKClient` - -The following type definitions are used in the `ReactSDKClient` interface: - -- `UserAttributes : { [name: string]: any }` -- `User : { id: string | null, attributes: userAttributes }` -- `VariableValuesObject : { [key: string]: any }` -- `EventTags : { [key: string]: string | number | boolean; }` - -`ReactSDKClient` instances have the methods/properties listed below. Note that in general, the API largely matches that of the core `@optimizely/optimizely-sdk` client instance, which is documented on the [Optimizely Feature Experimentation developer docs site](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/welcome). The major exception is that, for most methods, user id & attributes are **_optional_** arguments. `ReactSDKClient` has a current user. This user's id & attributes are automatically applied to all method calls, and overrides can be provided as arguments to these method calls if desired. - -| Method / Property | Signature | Description | -| --- | --- | --- | -| `onReady` | `(opts?: { timeout?: number }): Promise` | Returns a Promise that fulfills with an `onReadyResult` object representing the initialization process. The instance is ready when it has fetched a datafile and a user is available (via `setUser` being called with an object, or a Promise passed to `setUser` becoming fulfilled). If the `timeout` period happens before the client instance is ready, the `onReadyResult` object will contain an additional key, `dataReadyPromise`, which can be used to determine when, if ever, the instance does become ready. | -| `user` | `User` | The current user associated with this client instance. | -| `setUser` | `(userInfo: User \| Promise, qualifiedSegments?: string[]): Promise` | Call this to update the current user. Optionally pass `qualifiedSegments` to set pre-fetched ODP audience segments on the user context. | -| `onUserUpdate` | `(handler: (userInfo: User) => void): () => void` | Subscribe a callback to be called when this instance's current user changes. Returns a function that will unsubscribe the callback. | -| `decide` | `(key: string, options?: OptimizelyDecideOption[], overrideUserId?: string, overrideAttributes?: UserAttributes): OptimizelyDecision` | Returns a decision result for a flag key for a user. The decision result is returned in an `OptimizelyDecision` object, and contains all data required to deliver the flag rule. | -| `decideAll` | `(options?: OptimizelyDecideOption[], overrideUserId?: string, overrideAttributes?: UserAttributes): { [key: string]: OptimizelyDecision }` | Returns decisions for all active (unarchived) flags for a user. | -| `decideForKeys` | `(keys: string[], options?: OptimizelyDecideOption[], overrideUserId?: string, overrideAttributes?: UserAttributes): { [key: string]: OptimizelyDecision }` | Returns an object of decision results mapped by flag keys. | -| `activate` | `(experimentKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): string \| null` | Activate an experiment, and return the variation for the given user. | -| `getVariation` | `(experimentKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): string \| null` | Return the variation for the given experiment and user. | -| `getFeatureVariables` | `(featureKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): VariableValuesObject` | **Deprecated since 2.1.0.** Decide and return variable values for the given feature and user. Use `getAllFeatureVariables` instead. | -| `getFeatureVariableString` | `(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): string \| null` | Decide and return the variable value for the given feature, variable, and user. | -| `getFeatureVariableInteger` | `(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): number \| null` | Decide and return the variable value for the given feature, variable, and user. | -| `getFeatureVariableBoolean` | `(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): boolean \| null` | Decide and return the variable value for the given feature, variable, and user. | -| `getFeatureVariableDouble` | `(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): number \| null` | Decide and return the variable value for the given feature, variable, and user. | -| `isFeatureEnabled` | `(featureKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): boolean` | Return the enabled status for the given feature and user. | -| `getEnabledFeatures` | `(overrideUserId?: string, overrideAttributes?: UserAttributes): Array` | Return the keys of all features enabled for the given user. | -| `track` | `(eventKey: string, overrideUserId?: string \| EventTags, overrideAttributes?: UserAttributes, eventTags?: EventTags): void` | Track an event to the Optimizely results backend. | -| `setForcedVariation` | `(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string \| null): boolean` | Set a forced variation for the given experiment, variation, and user. **Note:** triggers a re-render of all `useExperiment` hooks and `OptimizelyExperiment` components using that client. | -| `getForcedVariation` | `(experiment: string, overrideUserId?: string): string \| null` | Get the forced variation for the given experiment, variation, and user. | - -## Rollout or experiment a feature user-by-user - -To rollout or experiment on a feature by user rather than by random percentage, you will use Attributes and Audiences. To do this, follow the documentation on how to [run a beta](https://docs.developers.optimizely.com/feature-experimentation/docs/run-a-beta) using the React code samples. - -## Server Side Rendering - -The React SDK supports server-side rendering (SSR). Pre-fetch the datafile and pass it to `createInstance` so decisions are available synchronously. Server-side instances are short-lived (created per request), so configure them to avoid unnecessary background work: - -```jsx -import { createInstance, OptimizelyProvider, OptimizelyDecideOption, useDecision } from '@optimizely/react-sdk'; - -function App() { - const isServerSide = typeof window === 'undefined'; - const [optimizely] = useState(() => - createInstance({ - datafile, - sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', - datafileOptions: { autoUpdate: !isServerSide }, - defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], - odpOptions: { - disabled: isServerSide, - }, - }) - ); -} - -function MyComponent() { - const [decision] = useDecision('flag1'); - return decision.enabled ?

Feature enabled

:

Feature disabled

; -} - - - -; -``` - -| Option | Server value | Why | -| ---------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------ | -| `datafile` | Pre-fetched datafile JSON | Provides the datafile directly so the SDK is ready synchronously to make decisions | -| `datafileOptions.autoUpdate` | `false` | No need to poll for datafile updates on a per-request instance | -| `defaultDecideOptions` | `[DISABLE_DECISION_EVENT]` | avoids duplicate decision events if the client will also fire them after hydration | -| `odpOptions.disabled` | `true` | Disables ODP event manager processing during SSR — avoids unnecessary event batching, API calls, and VUID tracking overhead | - -> **ODP audience segments during SSR:** Disabling ODP prevents automatic segment fetching, but you can still make audience-segment-based decisions by passing pre-fetched segments via the `qualifiedSegments` prop on `OptimizelyProvider`. - -### `getQualifiedSegments` - -A standalone async utility that fetches qualified ODP audience segments for a user, given a datafile. It parses the datafile to extract ODP configuration and segment conditions, queries the ODP GraphQL API, and returns only the segments where the user is qualified. - -```ts -import { getQualifiedSegments } from '@optimizely/react-sdk'; - -const segments = await getQualifiedSegments(userId, datafile); -``` - -| Argument | Type | Description | -| ---------- | --------------------------------- | ------------------------------------------------ | -| `userId` | `string` | The user ID to fetch qualified segments for | -| `datafile` | `string \| Record` | The Optimizely datafile (JSON object or string) | - -**Returns:** `Promise` - -> **Caching recommendation:** The ODP segment fetch adds latency to server rendering. Consider caching the result per user to avoid re-fetching on every request. - -### React Server Components (v3.4.0+) - -Since version 3.4.0, the SDK can be used directly in React Server Components without `OptimizelyProvider`. Create an instance, set the user, wait for readiness, and make decisions — all within an `async` server component: - -```tsx -import { createInstance } from '@optimizely/react-sdk'; - -export default async function ServerExperiment() { - const client = createInstance({ - sdkKey: process.env.OPTIMIZELY_SDK_KEY || '', - }); - - client.setUser({ - id: 'user-123', - }); - - await client.onReady(); - - const decision = client.decide('flag-1'); - - client.close(); - - return decision.enabled ?

Experiment Variation

:

Control

; -} -``` - -### Next.js Integration - -For detailed Next.js examples covering both App Router and Pages Router patterns, see the [Next.js Integration Guide](docs/nextjs-integration.md). - -### Limitations - -- **Datafile required** — SSR requires a pre-fetched datafile. Using `sdkKey` alone falls back to a failed decision. -- **User Promise not supported** — User `Promise` is not supported during SSR. -- **ODP segments** — ODP audience segments require async I/O and are not available during server rendering. Use [`getQualifiedSegments`](#getqualifiedsegments) to pre-fetch segments server-side and pass them via the `qualifiedSegments` prop on `OptimizelyProvider` to enable synchronous ODP-based decisions. Without it, consider deferring the decision to the client using the fallback pattern. - -For more details and workarounds, see the [Next.js Integration Guide — Limitations](docs/nextjs-integration.md#limitations). - -## Disabled event dispatcher - -To disable sending all events to Optimizely's results backend, use the `logOnlyEventDispatcher` when creating a client: - -```js -import { createInstance, logOnlyEventDispatcher } from '@optimizely/react-sdk'; - -const optimizely = createInstance({ - datafile: window.optimizelyDatafile, - eventDispatcher: logOnlyEventDispatcher, -}); -``` - -### Additional code - -This repository includes the following third party open source code: - -[**hoist-non-react-statics**](https://github.com/mridgway/hoist-non-react-statics) -Copyright © 2015 Yahoo!, Inc. -License: [BSD](https://github.com/mridgway/hoist-non-react-statics/blob/master/LICENSE.md) - -[**js-tokens**](https://github.com/lydell/js-tokens) -Copyright © 2014, 2015, 2016, 2017, 2018, 2019 Simon Lydell -License: [MIT](https://github.com/lydell/js-tokens/blob/master/LICENSE) - -[**json-schema**](https://github.com/kriszyp/json-schema) -Copyright © 2005-2015, The Dojo Foundation -License: [BSD](https://github.com/kriszyp/json-schema/blob/master/LICENSE) - -[**lodash**](https://github.com/lodash/lodash/) -Copyright © JS Foundation and other contributors -License: [MIT](https://github.com/lodash/lodash/blob/master/LICENSE) - -[**loose-envify**](https://github.com/zertosh/loose-envify) -Copyright © 2015 Andres Suarez -License: [MIT](https://github.com/zertosh/loose-envify/blob/master/LICENSE) - -[**node-murmurhash**](https://github.com/perezd/node-murmurhash) -Copyright © 2012 Gary Court, Derek Perez -License: [MIT](https://github.com/perezd/node-murmurhash/blob/master/README.md) - -[**object-assign**](https://github.com/sindresorhus/object-assign) -Copyright © Sindre Sorhus (sindresorhus.com) -License: [MIT](https://github.com/sindresorhus/object-assign/blob/master/license) - -[**promise-polyfill**](https://github.com/taylorhakes/promise-polyfill) -Copyright © 2014 Taylor Hakes -Copyright © 2014 Forbes Lindesay -License: [MIT](https://github.com/taylorhakes/promise-polyfill/blob/master/LICENSE) - -[**react-is**](https://github.com/facebook/react) -Copyright © Facebook, Inc. and its affiliates. -License: [MIT](https://github.com/facebook/react/blob/master/LICENSE) - -[**react**](https://github.com/facebook/react) -Copyright © Facebook, Inc. and its affiliates. -License: [MIT](https://github.com/facebook/react/blob/master/LICENSE) - -[**scheduler**](https://github.com/facebook/react) -Copyright © Facebook, Inc. and its affiliates. -License: [MIT](https://github.com/facebook/react/blob/master/LICENSE) - -[**node-uuid**](https://github.com/kelektiv/node-uuid) -Copyright © 2010-2016 Robert Kieffer and other contributors -License: [MIT](https://github.com/kelektiv/node-uuid/blob/master/LICENSE.md) - -To regenerate the dependencies use by this package, run the following command: - -```sh -npx license-checker --production --json | jq 'map_values({ licenses, publisher, repository }) | del(.[][] | nulls)' -``` - -### Contributing - -Please see [CONTRIBUTING](./CONTRIBUTING.md) for more information. - -### Credits - -First-party code subject to copyrights held by Optimizely, Inc. and its contributors and licensed to you under the terms of the Apache 2.0 license. - -### Other Optimizely SDKs - -- Agent - https://github.com/optimizely/agent - -- Android - https://github.com/optimizely/android-sdk - -- C# - https://github.com/optimizely/csharp-sdk - -- Flutter - https://github.com/optimizely/optimizely-flutter-sdk - -- Go - https://github.com/optimizely/go-sdk - -- Java - https://github.com/optimizely/java-sdk - -- JavaScript - https://github.com/optimizely/javascript-sdk - -- PHP - https://github.com/optimizely/php-sdk - -- Python - https://github.com/optimizely/python-sdk - -- Ruby - https://github.com/optimizely/ruby-sdk - -- Swift - https://github.com/optimizely/swift-sdk +# Optimizely React SDK + +This repository houses the React SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). + +Optimizely Feature Experimentation is an A/B testing and feature management tool for product development teams that enables you to experiment at every step. Using Optimizely Feature Experimentation allows for every feature on your roadmap to be an opportunity to discover hidden insights. Learn more at [Optimizely.com](https://www.optimizely.com/products/experiment/feature-experimentation/), or see the [developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/introduction). + +## Table of Contents + +- [Get Started](#get-started) +- [Installation](#installation) +- [Quick Example](#quick-example) +- [Usage](#usage) + - [Client Creation](#client-creation) + - [``](#optimizelyprovider) + - [Readiness](#readiness) + - [Hooks](#hooks) + - [Event Tracking](#event-tracking) + - [Forced Decisions](#forced-decisions) + - [Logging](#logging) +- [Server-Side Rendering](#server-side-rendering) + - [`getQualifiedSegments`](#getqualifiedsegments) + - [React Server Components](#react-server-components) + - [Next.js Integration](#nextjs-integration) + - [Limitations](#limitations) +- [Migrating from v3](#migrating-from-v3) +- [Additional Code](#additional-code) +- [Contributing](#contributing) + +## Get Started + +Refer to the [React SDK's developer documentation](https://docs.developers.optimizely.com/experimentation/v4.0.0-full-stack/docs/javascript-react-sdk) for detailed instructions on getting started with using the SDK. + +For React Native, review the [React Native developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-react-native-sdk). + +### Features + +- Modular, tree-shakeable architecture built on the JS SDK v6 +- Automatic datafile polling and hook re-evaluation on config changes +- User context managed by the Provider +- Async decision hooks for CMAB and async User Profile Service lookups +- Server-safe entry point for React Server Components +- Pre-built hooks for single-flag, multi-flag, and all-flag decisions + +### Compatibility + +| Requirement | Version | +| --- | --- | +| React | >= 16.8.0 | +| Node.js | >= 18.0.0 | +| Module format | ESM only | + +## Installation + +``` +npm install @optimizely/react-sdk +``` + +For **React Native**, the installation is slightly different. Check out the: + +- [Official Installation guide](https://docs.developers.optimizely.com/feature-experimentation/docs/install-sdk-reactnative) +- [Expo React Native Sample App](https://github.com/optimizely/expo-react-native-sdk-sample) + +## Quick Example + +```jsx +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + OptimizelyProvider, + useDecide, +} from '@optimizely/react-sdk'; + +const optimizelyClient = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-optimizely-sdk-key', + }), + eventProcessor: createBatchEventProcessor(), +}); + +function MyComponent() { + const { decision, isLoading, error } = useDecide('sort-algorithm'); + + if (isLoading || error) return null; + + return ( + <> + {decision.variationKey === 'relevant_first' && } + {decision.variationKey === 'recent_first' && } + + ); +} + + +function App() { + return ( + + + + ); +} +``` + +## Usage + +### Client Creation + +#### `createInstance` + +Creates an Optimizely `Client` instance that powers the Provider and hooks. The client does not hold user state — user management is handled by ``. + +> **Important:** You must use `createInstance` from `@optimizely/react-sdk`, not from `@optimizely/optimizely-sdk`. A client created directly from the JS SDK will not work correctly with `` and hooks. + +_arguments_ + +- `config : object` — Configuration object with the following fields: + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `projectConfigManager` | `OpaqueConfigManager` | Yes | Manages the datafile. Create with `createPollingProjectConfigManager` or `createStaticProjectConfigManager`. | +| `eventProcessor` | `OpaqueEventProcessor` | No | Processes and dispatches events. Create with `createBatchEventProcessor` or `createForwardingEventProcessor`. | +| `jsonSchemaValidator` | `{ validate(jsonObject: unknown): boolean }` | No | Custom validator for datafile schema validation. | +| `logger` | `OpaqueLogger` | No | Logger instance. Create with `createLogger`. | +| `errorNotifier` | `OpaqueErrorNotifier` | No | Error notification handler. Create with `createErrorNotifier`. | +| `userProfileService` | `UserProfileService` | No | Synchronous user profile service for sticky bucketing. | +| `userProfileServiceAsync` | `UserProfileServiceAsync` | No | Asynchronous user profile service for sticky bucketing. | +| `defaultDecideOptions` | `OptimizelyDecideOption[]` | No | Default options applied to all decide calls. | +| `clientEngine` | `string` | No | Custom client engine identifier. | +| `clientVersion` | `string` | No | Custom client version identifier. | +| `odpManager` | `OpaqueOdpManager` | No | ODP manager for audience segments and events. Create with `createOdpManager`. | +| `vuidManager` | `OpaqueVuidManager` | No | Visitor UID manager. Create with `createVuidManager`. | +| `disposable` | `boolean` | No | When `true`, marks the client as single-use for SSR. | +| `cmab` | `object` | No | CMAB cache configuration: `cacheSize` (number), `cacheTtl` (number, ms), `cache` (custom cache instance). | + +> For detailed configuration options for each factory (polling intervals, custom event dispatchers, ODP, VUID etc.), see the [JavaScript SDK initialization guide](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-the-javascript-sdk). + +_returns_ + +- A `Client` instance. + +```jsx +import { + createInstance, + createPollingProjectConfigManager, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-optimizely-sdk-key', + datafile: window.optimizelyDatafile, // optional: use as initial datafile while polling + autoUpdate: true, + }), +}); +``` + +#### Config manager factories + +| Factory | Use case | +| --- | --- | +| `createPollingProjectConfigManager({ sdkKey, datafile?, autoUpdate? })` | Fetches and polls for datafile updates. `sdkKey` is required. | +| `createStaticProjectConfigManager({ datafile })` | Uses a fixed datafile with no polling. | + +#### Event processor factories + +| Factory | Use case | +| --- | --- | +| `createBatchEventProcessor({ batchSize?, flushInterval? })` | Batches events before dispatching. | +| `createForwardingEventProcessor()` | Forwards each event immediately. | + +> **Note:** Event processing is opt-in. If no `eventProcessor` is passed to `createInstance`, no events are dispatched. + +#### Other configurable modules + +| Factory | Use case | +| --- | --- | +| `createOdpManager()` | Enables ODP integration (audience segments, events). | +| `createVuidManager()` | Enables visitor UID tracking. | +| `createErrorNotifier()` | Configures error notification. | +| `createLogger({ logLevel })` | Creates a logger instance (see [Logging](#logging)). | + +#### Full client creation example + +```jsx +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + createOdpManager, + createLogger, + DEBUG, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-sdk-key', + }), + eventProcessor: createBatchEventProcessor({ + batchSize: 10, + flushInterval: 2000, + }), + odpManager: createOdpManager(), + logger: createLogger({ logLevel: DEBUG }), +}); +``` + +### `` + +Required at the root level. Leverages React's `Context` API to provide the Optimizely client and user context to hooks. + +_props_ + +| Prop | Type | Required | Description | +| --- | --- | --- | --- | +| `client` | `Client` | Yes | Instance created from `createInstance`. | +| `user` | `{ id?: string; attributes?: UserAttributes }` | No | User info object — `id` and `attributes` will be used to create the user context for all decisions and event tracking. | +| `timeout` | `number` | No | Maximum time (in milliseconds) to wait for the SDK to become ready before hooks resolve with a loading state. Default: `30000`. | +| `qualifiedSegments` | `string[]` | No | Pre-fetched ODP audience segments for the user. Use [`getQualifiedSegments`](#getqualifiedsegments) to obtain these segments server-side. | +| `skipSegments` | `boolean` | No | When `true`, skips background ODP segment fetching. Default: `false`. | + +> **Note:** Unless VUID is enabled, `` requires user data. If user information must be fetched asynchronously, resolve the promise before rendering the Provider. + +### Readiness + +Before rendering real content, both the datafile and the user must be available to the SDK. + +#### With datafile + +Use `createStaticProjectConfigManager` to initialize with a pre-fetched datafile. The SDK is ready immediately with no network requests. + +The `datafile` parameter accepts either a valid JSON string or a parsed datafile object. + +```jsx +import { + OptimizelyProvider, + createInstance, + createStaticProjectConfigManager, + createForwardingEventProcessor, +} from '@optimizely/react-sdk'; + +// Using a JSON string (e.g., fetched server-side and injected into the page) +const optimizelyClient = createInstance({ + projectConfigManager: createStaticProjectConfigManager({ + datafile: '{"version": "4", "flags": ...}', + }), + eventProcessor: createForwardingEventProcessor(), +}); + +// Using a parsed object +const optimizelyClient = createInstance({ + projectConfigManager: createStaticProjectConfigManager({ + datafile: {"version": "4", "flags": ...}, + }), + eventProcessor: createForwardingEventProcessor(), +}); + +function AppWrapper() { + return ( + + + + ); +} +``` + +#### With SDK key + +Use `createPollingProjectConfigManager` with an `sdkKey` to have the SDK fetch and poll for the datafile. Provide a `timeout` option to `` so hooks can distinguish between the loading state and actual decisions. Hooks return `isLoading: true` while waiting and resolve once the SDK is ready or the timeout expires. + +```jsx +import { + OptimizelyProvider, + createInstance, + createPollingProjectConfigManager, + createForwardingEventProcessor, + useDecide, +} from '@optimizely/react-sdk'; + +const optimizelyClient = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-optimizely-sdk-key', + }), + eventProcessor: createForwardingEventProcessor(), +}); + +function MyComponent() { + const { decision, isLoading, error } = useDecide('the-flag'); + + if (isLoading) return null; + if (error) return
Error: {error.message}
; + + return
{decision.enabled ? 'Feature On' : 'Feature Off'}
; +} + +function App() { + return ( + + + + ); +} +``` + +#### With both SDK key and datafile + +You can provide both an `sdkKey` and a `datafile` to `createPollingProjectConfigManager`. The SDK uses the given datafile immediately, then fetches and polls for updates in the background. + +```jsx +import { + OptimizelyProvider, + createInstance, + createPollingProjectConfigManager, + createForwardingEventProcessor, +} from '@optimizely/react-sdk'; + +const optimizelyClient = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: 'your-optimizely-sdk-key', + datafile: window.optimizelyDatafile, + }), + eventProcessor: createForwardingEventProcessor(), +}); + +function AppWrapper() { + return ( + + + + ); +} +``` + +### Hooks + +#### `useDecide` + +The primary hook for retrieving a feature flag decision. Automatically re-evaluates when the underlying datafile changes. + +_arguments_ + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `flagKey` | `string` | Yes | The key of the feature flag. | +| `config` | `object` | No | Configuration object. | +| `config.decideOptions` | `OptimizelyDecideOption[]` | No | Array of decide options. | + +_returns_ `{ decision, isLoading, error }` + +| Field | Type | Description | +| --- | --- | --- | +| `decision` | `OptimizelyDecision \| null` | The decision result. `null` while loading or on error. | +| `isLoading` | `boolean` | `true` while waiting for the SDK to become ready. | +| `error` | `Error \| null` | Error object if the decision failed, otherwise `null`. | + +```jsx +import { useDecide } from '@optimizely/react-sdk'; + +function LoginComponent() { + const { decision, isLoading, error } = useDecide('login-flag'); + + if (isLoading) return ; + if (error) return ; + + return ( +

+ Click to login +

+ ); +} +``` + +#### `useDecideForKeys` + +Retrieve decisions for multiple flag keys in a single call. + +_arguments_ + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `flagKeys` | `string[]` | Yes | Array of flag keys. | +| `config` | `object` | No | Configuration object. | +| `config.decideOptions` | `OptimizelyDecideOption[]` | No | Array of decide options. | + +_returns_ `{ decisions, isLoading, error }` + +| Field | Type | Description | +| --- | --- | --- | +| `decisions` | `{ [key: string]: OptimizelyDecision } \| null` | Map of flag keys to their decisions. `null` while loading or on error. | +| `isLoading` | `boolean` | `true` while waiting for the SDK to become ready. | +| `error` | `Error \| null` | Error object if the decisions failed, otherwise `null`. | + +```jsx +import { useDecideForKeys } from '@optimizely/react-sdk'; + +function Dashboard() { + const { decisions, isLoading } = useDecideForKeys(['flag-a', 'flag-b']); + + if (isLoading) return ; + + return ( + <> + {decisions['flag-a']?.enabled && } + {decisions['flag-b']?.enabled && } + + ); +} +``` + +#### `useDecideAll` + +Retrieve decisions for all active (unarchived) flags. + +_arguments_ + +| Parameter | Type | Required | Description | +| --- | --- | --- | --- | +| `config` | `object` | No | Configuration object. | +| `config.decideOptions` | `OptimizelyDecideOption[]` | No | Array of decide options. | + +_returns_ `{ decisions, isLoading, error }` + +| Field | Type | Description | +| --- | --- | --- | +| `decisions` | `{ [key: string]: OptimizelyDecision } \| null` | Map of flag keys to their decisions. `null` while loading or on error. | +| `isLoading` | `boolean` | `true` while waiting for the SDK to become ready. | +| `error` | `Error \| null` | Error object if the decisions failed, otherwise `null`. | + +#### Async decision hooks + +Async variants call the underlying async SDK methods (`decideAsync`, `decideForKeysAsync`, `decideAllAsync`). Use these when your setup involves asynchronous operations such as CMAB (Contextual Multi-Armed Bandit) decisions or async User Profile Service lookups. + +| Hook | Description | +| --- | --- | +| `useDecideAsync(flagKey, config?)` | Async variant of `useDecide` | +| `useDecideForKeysAsync(flagKeys[], config?)` | Async variant of `useDecideForKeys` | +| `useDecideAllAsync(config?)` | Async variant of `useDecideAll` | + +These hooks have the same return types as their synchronous counterparts. + +```jsx +import { useDecideAsync } from '@optimizely/react-sdk'; + +function MyComponent() { + const { decision, isLoading, error } = useDecideAsync('flag-key'); + + if (isLoading) return ; + if (error) return ; + + return decision.enabled ? : ; +} +``` + +#### `useOptimizelyClient` + +Returns the Optimizely `Client` instance from context for direct SDK access. + +```jsx +import { useOptimizelyClient } from '@optimizely/react-sdk'; + +function MyComponent() { + const client = useOptimizelyClient(); + // Use client methods directly +} +``` + +#### `useOptimizelyUserContext` + +Returns the current user context, which can be used for event tracking and forced decisions. + +_returns_ + +- `{ userContext, isLoading, error }` — Where `userContext` is an Optimizely `UserContext` object. + +```jsx +import { useOptimizelyUserContext } from '@optimizely/react-sdk'; + +function MyComponent() { + const { userContext } = useOptimizelyUserContext(); + + const handleClick = () => { + userContext?.trackEvent('button-clicked', { revenue: 100 }); + }; + + return ; +} +``` + +### Event Tracking + +Use the `useOptimizelyUserContext` hook to track events: + +```jsx +import { useOptimizelyUserContext } from '@optimizely/react-sdk'; + +function SignupButton() { + const { userContext } = useOptimizelyUserContext(); + + const handleClick = () => { + userContext?.trackEvent('signup-clicked'); + }; + + return ; +} +``` + +### Forced Decisions + +Forced decisions are set on the `userContext` object: + +```jsx +import { useOptimizelyUserContext } from '@optimizely/react-sdk'; + +function MyComponent() { + const { userContext } = useOptimizelyUserContext(); + + // Set a forced decision + userContext?.setForcedDecision( + { flagKey: 'flag-1', ruleKey: 'rule-1' }, + { variationKey: 'variation-a' } + ); + + // Remove a forced decision + userContext?.removeForcedDecision({ flagKey: 'flag-1', ruleKey: 'rule-1' }); + + // Remove all forced decisions + userContext?.removeAllForcedDecisions(); +} +``` + +Hooks that use the affected flag key automatically re-render when forced decisions change. + +### Logging + +Logging is disabled by default. Pass a `logger` to `createInstance` to enable it: + +```jsx +import { + createInstance, + createPollingProjectConfigManager, + createLogger, + DEBUG, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ sdkKey: 'your-sdk-key' }), + logger: createLogger({ logLevel: DEBUG }), +}); +``` + +Log level constants `DEBUG`, `INFO`, `WARN`, and `ERROR` are exported for convenience. + +## Server-Side Rendering + +The React SDK supports server-side rendering (SSR). A pre-fetched datafile is required for SSR — without one, the SDK cannot make decisions during server rendering. + +#### Per-request client + +Create a client inside the component with `createStaticProjectConfigManager` and a pre-fetched datafile. Use `useMemo` to avoid recreating the instance on re-renders, and `disposable: true` so the instance can be garbage collected without explicitly calling `close()`. + +```jsx +import { useMemo } from 'react'; +import { + createInstance, + createStaticProjectConfigManager, + OptimizelyProvider, + useDecide, +} from '@optimizely/react-sdk'; + +export default function Page({ datafile, userId }) { + const optimizely = useMemo( + () => + createInstance({ + projectConfigManager: createStaticProjectConfigManager({ datafile }), + disposable: true, + }), + [datafile] + ); + + return ( + + + + ); +} + +function MyComponent() { + const { decision, isLoading } = useDecide('flag1'); + if (isLoading) return null; + return decision.enabled ?

Feature enabled

:

Feature disabled

; +} +``` + +#### Module-level client + +For a long-lived server process, create a singleton client at the module level with `createPollingProjectConfigManager`. The client fetches and polls for datafile updates in the background. + +Provide a datafile for immediate readiness. Without one, the server may render decisions once the initial fetch completes, but the client will not have the datafile yet (fetch still in-flight) and will render a loading or default state — causing a hydration mismatch. + +If an `eventProcessor` is configured on the server, use `DISABLE_DECISION_EVENT` to avoid firing duplicate decision events (the client will fire them after hydration). + +```jsx +import { + createInstance, + createPollingProjectConfigManager, + createBatchEventProcessor, + OptimizelyProvider, + OptimizelyDecideOption, + useDecide, +} from '@optimizely/react-sdk'; + +const optimizely = createInstance({ + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY, + datafile: preloadedDatafile, + }), + eventProcessor: createBatchEventProcessor(), + defaultDecideOptions: [OptimizelyDecideOption.DISABLE_DECISION_EVENT], +}); + +function MyComponent() { + const { decision, isLoading } = useDecide('flag1'); + if (isLoading) return null; + return decision.enabled ?

Feature enabled

:

Feature disabled

; +} + +function App() { + return ( + + + + ); +} +``` + +### `getQualifiedSegments` + +A standalone async utility that fetches qualified ODP audience segments for a user, given a datafile. It parses the datafile to extract ODP configuration and segment conditions, queries the ODP GraphQL API, and returns only the segments where the user is qualified. + +```ts +import { getQualifiedSegments } from '@optimizely/react-sdk'; + +const { segments, error } = await getQualifiedSegments(userId, datafile); +``` + +| Argument | Type | Description | +| --- | --- | --- | +| `userId` | `string` | The user ID to fetch qualified segments for | +| `datafile` | `string \| Record` | The Optimizely datafile (JSON object or string) | + +**Returns:** `Promise` — `{ segments: string[], error: Error | null }` + +> **Caching recommendation:** The ODP segment fetch adds latency to server rendering. Consider caching the result per user to avoid re-fetching on every request. + +Pass the pre-fetched segments to the Provider: + +```jsx + + + +``` + +### React Server Components + +The SDK provides a server-safe entry point via the `react-server` export condition. Frameworks that support this condition (e.g., Next.js App Router) automatically resolve `@optimizely/react-sdk` to the server entry point when importing from a Server Component. This entry point excludes hooks and Provider (which use client-only React APIs). + +```tsx +import { createInstance, createStaticProjectConfigManager } from '@optimizely/react-sdk'; + +export default async function ServerComponent() { + const client = createInstance({ + projectConfigManager: createStaticProjectConfigManager({ datafile }), + }); + + await client.onReady(); + + const userContext = client.createUserContext('user-123'); + const decision = userContext.decide('flag-key'); + + client.close(); + + return decision.enabled ? : ; +} +``` + +### Next.js Integration + +For detailed Next.js examples covering both App Router and Pages Router patterns, see the [Next.js Integration Guide](docs/nextjs-integration.md). + +### Limitations + +- **Datafile required** — SSR requires a pre-fetched datafile. Using `sdkKey` alone falls back to a failed decision. +- **ODP segments** — ODP audience segments require async I/O and are not available during server rendering. Use [`getQualifiedSegments`](#getqualifiedsegments) to pre-fetch segments server-side and pass them via the `qualifiedSegments` prop on `OptimizelyProvider`. + +For more details and workarounds, see the [Next.js Integration Guide — Limitations](docs/nextjs-integration.md#limitations). + +## Migrating from v3 + +For a comprehensive migration guide covering all breaking changes, see [MIGRATION.md](./MIGRATION.md). + +## Rollout or Experiment a Feature User-by-User + +To rollout or experiment on a feature by user rather than by random percentage, you will use Attributes and Audiences. To do this, follow the documentation on how to [run a beta](https://docs.developers.optimizely.com/feature-experimentation/docs/run-a-beta) using the React code samples. + +## Additional Code + +This repository includes the following third party open source code: + +[**decompress-response**](https://github.com/sindresorhus/decompress-response) +Copyright © Sindre Sorhus +License: [MIT](https://github.com/sindresorhus/decompress-response/blob/main/license) + +[**js-tokens**](https://github.com/lydell/js-tokens) +Copyright © Simon Lydell +License: [MIT](https://github.com/lydell/js-tokens/blob/master/LICENSE) + +[**json-schema**](https://github.com/kriszyp/json-schema) +Copyright © Kris Zyp +License: [AFL-2.1 OR BSD-3-Clause](https://github.com/kriszyp/json-schema/blob/master/LICENSE) + +[**loose-envify**](https://github.com/zertosh/loose-envify) +Copyright © Andres Suarez +License: [MIT](https://github.com/zertosh/loose-envify/blob/master/LICENSE) + +[**mimic-response**](https://github.com/sindresorhus/mimic-response) +Copyright © Sindre Sorhus +License: [MIT](https://github.com/sindresorhus/mimic-response/blob/main/license) + +[**murmurhash**](https://github.com/perezd/node-murmurhash) +License: [MIT](https://github.com/perezd/node-murmurhash/blob/master/README.md) + +[**react**](https://github.com/facebook/react) +Copyright © Meta Platforms, Inc. and affiliates. +License: [MIT](https://github.com/facebook/react/blob/main/LICENSE) + +[**tslib**](https://github.com/Microsoft/tslib) +Copyright © Microsoft Corp. +License: [0BSD](https://github.com/nicolo-ribaudo/tslib/blob/main/LICENSE.txt) + +[**use-sync-external-store**](https://github.com/facebook/react) +Copyright © Meta Platforms, Inc. and affiliates. +License: [MIT](https://github.com/facebook/react/blob/main/LICENSE) + +[**uuid**](https://github.com/uuidjs/uuid) +License: [MIT](https://github.com/uuidjs/uuid/blob/main/LICENSE.md) + +To regenerate the dependencies used by this package, run the following command: + +```sh +npx license-checker --production --json | jq 'map_values({ licenses, publisher, repository }) | del(.[][] | nulls)' +``` + +## Contributing + +Please see [CONTRIBUTING](./CONTRIBUTING.md) for more information. + +## Credits + +First-party code subject to copyrights held by Optimizely, Inc. and its contributors and licensed to you under the terms of the Apache 2.0 license. + +## Other Optimizely SDKs + +- Agent - https://github.com/optimizely/agent +- Android - https://github.com/optimizely/android-sdk +- C# - https://github.com/optimizely/csharp-sdk +- Flutter - https://github.com/optimizely/optimizely-flutter-sdk +- Go - https://github.com/optimizely/go-sdk +- Java - https://github.com/optimizely/java-sdk +- JavaScript - https://github.com/optimizely/javascript-sdk +- PHP - https://github.com/optimizely/php-sdk +- Python - https://github.com/optimizely/python-sdk +- Ruby - https://github.com/optimizely/ruby-sdk +- Swift - https://github.com/optimizely/swift-sdk diff --git a/docs/nextjs-integration.md b/docs/nextjs-integration.md index 9852da4..cbad4f7 100644 --- a/docs/nextjs-integration.md +++ b/docs/nextjs-integration.md @@ -2,6 +2,24 @@ This guide covers how to use the Optimizely React SDK with Next.js for server-side rendering (SSR), static site generation (SSG), and React Server Components. +## Table of Contents + +- [Prerequisites](#prerequisites) +- [SSR with Pre-fetched Datafile](#ssr-with-pre-fetched-datafile) +- [Next.js App Router](#nextjs-app-router) + - [Create a datafile fetcher](#1-create-a-datafile-fetcher) + - [Create a client-side provider](#2-create-a-client-side-provider) + - [Module-level alternative](#module-level-alternative) + - [Wire it up in your root layout](#3-wire-it-up-in-your-root-layout) + - [Pre-fetching ODP audience segments](#pre-fetching-odp-audience-segments) +- [Next.js Pages Router](#nextjs-pages-router) +- [Using Feature Flags in Client Components](#using-feature-flags-in-client-components) +- [Static Site Generation (SSG)](#static-site-generation-ssg) +- [Limitations](#limitations) + - [Datafile required for SSR](#datafile-required-for-ssr) + - [User Promise not supported](#user-promise-not-supported) + - [ODP audience segments](#odp-audience-segments) + ## Prerequisites Install the React SDK: @@ -26,24 +44,23 @@ In the App Router, fetch the datafile in an async server component (e.g., your r **Option A: Using the SDK's built-in datafile fetching (Recommended)** -Create a module-level SDK instance with your `sdkKey` and use a notification listener to detect when the datafile is ready. This approach benefits from the SDK's built-in polling and caching, making it suitable when you want automatic datafile updates across requests. +Create a module-level SDK instance with a polling config manager and use a notification listener to detect when the datafile is ready. This approach benefits from the SDK's built-in polling and caching, making it suitable when you want automatic datafile updates across requests. ```ts // src/data/getDatafile.ts -import { createInstance } from '@optimizely/react-sdk'; +import { createInstance, createPollingProjectConfigManager, NOTIFICATION_TYPES } from '@optimizely/react-sdk'; const pollingInstance = createInstance({ - sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || "", + projectConfigManager: createPollingProjectConfigManager({ + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', + }), }); -const pollingInstance = createInstane(); - const configReady = new Promise((resolve) => { - pollingInstance.notificationCenter.addNotificationListener( - enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, - () => resolve(); + pollingInstance.notificationCenter.addNotificationListener(NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, () => + resolve() ); -} +}); export function getDatafile(): Promise { return configReady.then(() => pollingInstance.getOptimizelyConfig()?.getDatafile()); @@ -77,36 +94,153 @@ Since `OptimizelyProvider` uses React Context (a client-side feature), it must b // src/providers/OptimizelyProvider.tsx 'use client'; -import { OptimizelyProvider, createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk'; +import { + OptimizelyProvider, + createInstance, + createStaticProjectConfigManager, + createPollingProjectConfigManager, + createBatchEventProcessor, + OptimizelyDecideOption, +} from '@optimizely/react-sdk'; import { ReactNode, useState } from 'react'; export function OptimizelyClientProvider({ children, datafile }: { children: ReactNode; datafile: object }) { - const isServerSide = typeof window === 'undefined'; + const isServerSide = typeof window === 'undefined'; - const [optimizely] = useState(() => + const [optimizely] = useState(() => createInstance({ - datafile, - sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', - datafileOptions: { autoUpdate: !isServerSide }, + projectConfigManager: isServerSide + ? createStaticProjectConfigManager({ datafile }) + : createPollingProjectConfigManager({ + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', + datafile, + }), + eventProcessor: isServerSide ? undefined : createBatchEventProcessor(), defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], - odpOptions: { - disabled: isServerSide, - }, + disposable: isServerSide, }) ); return ( - + + {children} + + ); +} +``` + +#### Module-level alternative + +You can also create the client at module level to avoid recreating the instance on re-renders. The trade-off is that the datafile must be resolved before the provider module is evaluated. One approach is to use `globalThis` to bridge the datafile between server and client: the server fetches the datafile, sets it on `globalThis` for server rendering, and injects a `