From 37c3982934272a06879e59d3851a2a2dcbbf8267 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 6 Mar 2026 23:10:19 +0600 Subject: [PATCH 1/2] [FSSDK-10777] doc update --- README.md | 222 ++++++++++++++----------- docs/nextjs-integration.md | 331 +++++++++++++++++++++++++++++++++++++ 2 files changed, 459 insertions(+), 94 deletions(-) create mode 100644 docs/nextjs-integration.md diff --git a/README.md b/README.md index 9cd42c5..75906c2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ Refer to the [React SDK's developer documentation](https://docs.developers.optim 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 @@ -28,11 +27,7 @@ The React SDK is compatible with `React 16.8.0 +` ### Example ```jsx -import { - createInstance, - OptimizelyProvider, - useDecision, -} from '@optimizely/react-sdk'; +import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/react-sdk'; const optimizelyClient = createInstance({ sdkKey: 'your-optimizely-sdk-key', @@ -43,8 +38,8 @@ function MyComponent() { return ( - { decision.variationKey === 'relevant_first' && } - { decision.variationKey === 'recent_first' && } + {decision.variationKey === 'relevant_first' && } + {decision.variationKey === 'recent_first' && } ); } @@ -70,7 +65,8 @@ class App extends React.Component { npm install @optimizely/react-sdk ``` -For **React Native**, installation instruction is bit different. Check out the +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) @@ -106,12 +102,15 @@ Required at the root level. Leverages React’s `Context` API to allow access to _props_ -- `optimizely : ReactSDKClient` created from `createInstance` -- `user: { id: string; attributes?: { [key: string]: any } } | Promise` User info object - `id` and `attributes` will be passed to the SDK for every feature flag, A/B test, or `track` call, or a `Promise` for the same kind of object -- `timeout : Number` (optional) The amount of time for `useDecision` to return `null` flag Decision while waiting for the SDK instance to become ready, before resolving. -- `isServerSide : Boolean` (optional) must pass `true` here for server side rendering -- `userId : String` (optional) **_Deprecated, prefer using `user` instead_**. Another way to provide user id. The `user` object prop takes precedence when both are provided. -- `userAttributes : Object` : (optional) **_Deprecated, prefer using `user` instead_**. Another way to provide user attributes. The `user` object prop takes precedence when both are provided. +| 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 @@ -155,9 +154,9 @@ 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 */ } + {isClientReady &&
The Component
} + {didTimeout &&
Default Component
} + {/* If client is not ready and time out has not occured yet, do not render anything */}
); } @@ -277,7 +276,7 @@ class MyComp extends React.Component { constructor(props) { super(props); const { optimizely } = this.props; - const decision = optimizely.decide('feat1'); + const decision = optimizely.decide('feat1'); this.state = { decision.enabled, @@ -298,9 +297,11 @@ const WrappedMyComponent = withOptimizely(MyComp); 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` @@ -321,10 +322,10 @@ function MyComponent() { }; 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

} + {decision.enabled &&

My feature is enabled

} + {!decision.enabled &&

My feature is disabled

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

Current Variation

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

Better Variation

} ); @@ -332,23 +333,22 @@ function MyComponent() { ``` ### 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 [track, clientReady, didTimeout] = useTrackEvent(); const handleClick = () => { - if(clientReady) { - track('signup-clicked') + if (clientReady) { + track('signup-clicked'); } - } + }; - return ( - - ) + return ; } ``` @@ -385,25 +385,27 @@ The following type definitions are used in the `ReactSDKClient` interface: `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. -- `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): void` Call this to update the current user -- `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?: optimizely.OptimizelyDecideOption[], overrideUserId?: string, overrideAttributes?: optimizely.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?: optimizely.OptimizelyDecideOption[], overrideUserId?: string, overrideAttributes?: optimizely.UserAttributes): { [key: string]: OptimizelyDecision }` Returns decisions for all active (unarchived) flags for a user. -- `decideForKeys(keys: string[], options?: optimizely.OptimizelyDecideOption[], overrideUserId?: string, overrideAttributes?: optimizely.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`: Decide and return variable values for the given feature and user
Warning: Deprecated since 2.1.0
`getAllFeatureVariables` is added in JavaScript SDK which is similarly returning all the feature variables, but it sends only single notification of type `all-feature-variables` instead of sending for each variable. As `getFeatureVariables` was added when this functionality wasn't provided by `JavaScript SDK`, so there is no need of it now and it would be removed in next major release -- `getFeatureVariableString(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: optimizely.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**: calling `setForcedVariation` on a given client will trigger a re-render of all `useExperiment` hooks and `OptimizelyExperiment` components that are using that client. -- `getForcedVariation(experiment: string, overrideUserId?: string): string | null` Get the forced variation for the given experiment, variation, and user +| 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 @@ -411,70 +413,102 @@ To rollout or experiment on a feature by user rather than by random percentage, ## Server Side Rendering -Right now server side rendering is possible with a few caveats. +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: -**Caveats** +```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, + }, + }) + ); +} -1. You must download the datafile manually and pass in via the `datafile` option. Can not use `sdkKey` to automatically download. +function MyComponent() { + const [decision] = useDecision('flag1'); + return decision.enabled ?

Feature enabled

:

Feature disabled

; +} -2. Rendering of components must be completely synchronous (this is true for all server side rendering), thus the Optimizely SDK assumes that the optimizely client has been instantiated and fired it's `onReady` event already. + + +; +``` -### Setting up `` +| 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 | -Similar to browser side rendering you will need to wrap your app (or portion of the app using Optimizely) in the `` component. A new prop -`isServerSide` must be equal to true. +> **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`. -```jsx - - - +### `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); ``` -All other Optimizely components, such as `` and `` can remain the same. +| Argument | Type | Description | +| ---------- | --------------------------------- | ------------------------------------------------ | +| `userId` | `string` | The user ID to fetch qualified segments for | +| `datafile` | `string \| Record` | The Optimizely datafile (JSON object or string) | -### Full example +**Returns:** `Promise` -```jsx -import * as React from 'react'; -import * as ReactDOMServer from 'react-dom/server'; +> **Caching recommendation:** The ODP segment fetch adds latency to server rendering. Consider caching the result per user to avoid re-fetching on every request. -import { - createInstance, - OptimizelyProvider, - useDecision, -} from '@optimizely/react-sdk'; +### React Server Components -const fetch = require('node-fetch'); +The SDK can also 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: -function MyComponent() { - const [decision] = useDecision('flag1'); - return ( - - { decision.enabled &&

The feature is enabled

} - { !decision.enabled &&

The feature is not enabled

} - { decision.variationKey === 'variation1' &&

Variation 1

} - { decision.variationKey === 'variation2' &&

Variation 2

} -
- ); -} +```tsx +import { createInstance } from '@optimizely/react-sdk'; -async function main() { - const resp = await fetch('https://cdn.optimizely.com/datafiles/.json'); - const datafile = await resp.json(); - const optimizelyClient = createInstance({ - datafile, +export default async function ServerExperiment() { + const client = createInstance({ + sdkKey: process.env.OPTIMIZELY_SDK_KEY || '', }); - const output = ReactDOMServer.renderToString( - - - - ); - console.log('output', output); + client.setUser({ + id: 'user-123', + }); + + await client.onReady(); + + const decision = client.decide('flag-1'); + + client.close(); + + return decision.enabled ?

Experiment Variation

:

Control

; } -main(); ``` +### 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: diff --git a/docs/nextjs-integration.md b/docs/nextjs-integration.md new file mode 100644 index 0000000..9852da4 --- /dev/null +++ b/docs/nextjs-integration.md @@ -0,0 +1,331 @@ +# Next.js Integration Guide + +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. + +## Prerequisites + +Install the React SDK: + +```bash +npm install @optimizely/react-sdk +``` + +You will need your Optimizely SDK key, available from the Optimizely app under **Settings > Environments**. + +## SSR with Pre-fetched Datafile + +Server-side rendering requires a pre-fetched datafile. The SDK cannot fetch the datafile asynchronously during server rendering, so you must fetch it beforehand and pass it to `createInstance`. + +There are several ways to pre-fetch the datafile on the server. Below are two common approaches you could follow. + +## Next.js App Router + +In the App Router, fetch the datafile in an async server component (e.g., your root layout) and pass it as a prop to a client-side provider. + +### 1. Create a datafile fetcher + +**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. + +```ts +// src/data/getDatafile.ts +import { createInstance } from '@optimizely/react-sdk'; + +const pollingInstance = createInstance({ + 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(); + ); +} + +export function getDatafile(): Promise { + return configReady.then(() => pollingInstance.getOptimizelyConfig()?.getDatafile()); +} +``` + +**Option B: Direct CDN fetch** + +Fetch the datafile directly from CDN. + +```ts +// src/data/getDatafile.ts +const CDN_URL = `https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`; + +export async function getDatafile() { + const res = await fetch(CDN_URL); + + if (!res.ok) { + throw new Error(`Failed to fetch datafile: ${res.status}`); + } + + return res.json(); +} +``` + +### 2. Create a client-side provider + +Since `OptimizelyProvider` uses React Context (a client-side feature), it must be wrapped in a `'use client'` component: + +```tsx +// src/providers/OptimizelyProvider.tsx +'use client'; + +import { OptimizelyProvider, createInstance, 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 [optimizely] = useState(() => + createInstance({ + datafile, + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', + datafileOptions: { autoUpdate: !isServerSide }, + defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], + odpOptions: { + disabled: isServerSide, + }, + }) + ); + + return ( + + {children} + + ); +} +``` + +> See [Configuring the instance for server use](../README.md#configuring-the-instance-for-server-use) in the README for an explanation of each option. + +### 3. Wire it up in your root layout + +```tsx +// src/app/layout.tsx +import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider'; +import { getDatafile } from '@/data/getDatafile'; + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const datafile = await getDatafile(); + + return ( + + + {children} + + + ); +} +``` + +#### Pre-fetching ODP audience segments + +If your project uses ODP audience segments, you can pre-fetch them server-side using `getQualifiedSegments` and pass them to the provider via the `qualifiedSegments` prop. + +```tsx +// src/app/layout.tsx +import { getQualifiedSegments } from '@optimizely/react-sdk'; + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + const datafile = await getDatafile(); + const segments = await getQualifiedSegments('user-123', datafile); + + return ( + + + + {children} + + + + ); +} +``` + +> **Caching recommendation:** The ODP segment fetch adds latency to initial page loads. Consider caching the result per user to avoid re-fetching on every request. + +## Next.js Pages Router + +In the Pages Router, fetch the datafile server-side and pass it as a prop. There are three data-fetching strategies depending on your needs. + +### 1. Create a client-side provider + +Same as the [App Router provider](#2-create-a-client-side-provider) above (without the `'use client'` directive, which is not needed in Pages Router). + +### 2. Fetch the datafile + +Choose the data-fetching strategy that best fits your use case: + +#### Option A: `getInitialProps` — app-wide setup + +Fetches the datafile for every page via `_app.tsx`. Useful when you want Optimizely available globally across all pages. + +```tsx +// pages/_app.tsx +import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider'; +import type { AppProps, AppContext } from 'next/app'; +import { getDatafile } from '@/data/getDatafile'; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); +} + +App.getInitialProps = async (appContext: AppContext) => { + const appProps = await App.getInitialProps(appContext); + const datafile = await getDatafile(); + return { ...appProps, pageProps: { ...appProps.pageProps, datafile } }; +}; +``` + +Similar to App Router example, if you have ODP enabled and want to pre-fetch segments, you can do following - + +```tsx +import { getQualifiedSegments } from "@optimizely/react-sdk"; + +App.getInitialProps = async (appContext: AppContext) => { + const appProps = await App.getInitialProps(appContext); + const datafile = await getDatafile(); + const segments = await getQualifiedSegments('user-123', datafile); + return { ...appProps, pageProps: { ...appProps.pageProps, datafile, segments } }; +}; +``` + + +#### Option B: `getServerSideProps` — per-page setup + +Fetches the datafile per request on specific pages. Useful when only certain pages need feature flags. + +```tsx +// pages/index.tsx +export async function getServerSideProps() { + const datafile = await getDatafile(); + + return { props: { datafile } }; +} +``` + +#### Option C: `getStaticProps` — static generation with revalidation + +Fetches the datafile at build time and revalidates periodically. Best for static pages where per-request freshness is not critical. + +```tsx +// pages/index.tsx +export async function getStaticProps() { + const datafile = await getDatafile(); + + return { + props: { datafile }, + revalidate: 60, // re-fetch every 60 seconds + }; +} +``` + +## Using Feature Flags in Client Components + +Once the provider is set up, use the `useDecision` hook in any client component: + +```tsx +'use client'; + +import { useDecision } from '@optimizely/react-sdk'; + +export default function FeatureBanner() { + const [decision] = useDecision('banner-flag'); + + return decision.enabled ?

New Banner

:

Default Banner

; +} +``` + +## Static Site Generation (SSG) + +For statically generated pages, the SDK cannot make decisions during the build because there is no per-user context at build time. Instead, use the SDK as a regular client-side React library — the static HTML serves a default or loading state, and decisions resolve on the client after hydration. + +```tsx +'use client'; + +import { OptimizelyProvider, createInstance, useDecision } from '@optimizely/react-sdk'; + +const optimizely = createInstance({ sdkKey: 'YOUR_SDK_KEY' }); + +export function App() { + return ( + + + + ); +} + +function FeatureBanner() { + const [decision, isClientReady, didTimeout] = useDecision('banner-flag'); + + if (!isClientReady && !didTimeout) { + return

Loading...

; + } + + return decision.enabled ?

New Banner

:

Default Banner

; +} +``` + +## Limitations + +### Datafile required for SSR + +SSR with `sdkKey` alone (without a pre-fetched datafile) is **not supported** because it requires an asynchronous network call that cannot complete during synchronous server rendering. If no datafile is provided, decisions will fall back to defaults. + +To handle this gracefully, render a loading state and let the client hydrate with the real decision: + +```tsx +'use client'; + +import { useDecision } from '@optimizely/react-sdk'; + +export default function MyFeature() { + const [decision, isClientReady, didTimeout] = useDecision('flag-1'); + + if (!didTimeout && !isClientReady) { + return

Loading...

; + } + + return decision.enabled ?

Feature Enabled

:

Feature Disabled

; +} +``` + +### User Promise not supported + +User `Promise` is not supported during SSR. You must provide a static user object to `OptimizelyProvider`: + +```tsx +// Supported + + +// NOT supported during SSR + +``` + +### ODP audience segments + +ODP (Optimizely Data Platform) audience segments require fetching segment data via an async network call, which is not available during server rendering. To include segment data during SSR, pass pre-fetched segments via the `qualifiedSegments` prop on `OptimizelyProvider`: + +```tsx + + {children} + +``` + +This enables synchronous ODP-based decisions during server rendering. If `qualifiedSegments` is not provided, decisions will be made without audience segment data — in that case, consider deferring the decision to the client using the loading state fallback pattern described above, where ODP segments are fetched automatically when ODP is enabled. From 39cb8bf5908a231a43a645f3cb8152c17b5950a1 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:26:59 +0600 Subject: [PATCH 2/2] [FSSDK-10777] doc update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75906c2..6df1eb2 100644 --- a/README.md +++ b/README.md @@ -471,9 +471,9 @@ const segments = await getQualifiedSegments(userId, datafile); > **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 +### React Server Components (v3.4.0+) -The SDK can also 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: +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';