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.