From 6165ef743252024389b977454e50d61de19aba45 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:23:05 +0600 Subject: [PATCH 01/13] [FSSDK-12249] gitignore update --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 69897ad..11cb45c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ lib .npmrc dist/ build/ +.build/ +.github/prompts/ .rpt2_cache .env From 6a836f5e6d3afd31f9fb3e1f134f5b138d4dbb4a Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:17:31 +0600 Subject: [PATCH 02/13] [FSSDK-10777] ssr support update --- src/Experiment.spec.tsx | 7 +++++++ src/Feature.spec.tsx | 7 +++++++ src/hooks.spec.tsx | 2 ++ src/hooks.ts | 10 ++++++---- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Experiment.spec.tsx b/src/Experiment.spec.tsx index 3686538..7e6f166 100644 --- a/src/Experiment.spec.tsx +++ b/src/Experiment.spec.tsx @@ -63,6 +63,8 @@ describe('', () => { getIsUsingSdkKey: () => true, onForcedVariationsUpdate: jest.fn().mockReturnValue(() => {}), setUser: jest.fn(), + getOptimizelyConfig: jest.fn().mockImplementation(() => (isReady ? {} : null)), + getUserContext: jest.fn().mockImplementation(() => (isReady ? {} : null)), } as unknown as ReactSDKClient; }); @@ -512,6 +514,11 @@ describe('', () => { }); describe('when the isServerSide prop is true', () => { + beforeEach(() => { + (optimizelyMock.getOptimizelyConfig as jest.Mock).mockReturnValue({}); + (optimizelyMock.getUserContext as jest.Mock).mockReturnValue({}); + }); + it('should immediately render the result of the experiment without waiting', async () => { render( diff --git a/src/Feature.spec.tsx b/src/Feature.spec.tsx index 2de8810..1fe1340 100644 --- a/src/Feature.spec.tsx +++ b/src/Feature.spec.tsx @@ -60,6 +60,8 @@ describe('', () => { isReady: jest.fn().mockImplementation(() => isReady), getIsReadyPromiseFulfilled: () => true, getIsUsingSdkKey: () => true, + getOptimizelyConfig: jest.fn().mockImplementation(() => (isReady ? {} : null)), + getUserContext: jest.fn().mockImplementation(() => (isReady ? {} : null)), } as unknown as ReactSDKClient; }); @@ -310,6 +312,11 @@ describe('', () => { }); describe('when the isServerSide prop is true', () => { + beforeEach(() => { + (optimizelyMock.getOptimizelyConfig as jest.Mock).mockReturnValue({}); + (optimizelyMock.getUserContext as jest.Mock).mockReturnValue({}); + }); + it('should immediately render the result of isFeatureEnabled and getFeatureVariables', async () => { const { container } = render( diff --git a/src/hooks.spec.tsx b/src/hooks.spec.tsx index d43042f..aa8c344 100644 --- a/src/hooks.spec.tsx +++ b/src/hooks.spec.tsx @@ -172,6 +172,8 @@ describe('hooks', () => { setForcedDecision: setForcedDecisionMock, track: jest.fn(), setUser: jest.fn(), + getOptimizelyConfig: jest.fn().mockImplementation(() => (readySuccess ? {} : null)), + getUserContext: jest.fn().mockImplementation(() => (readySuccess ? {} : null)), } as unknown as ReactSDKClient; mockLog = jest.fn(); diff --git a/src/hooks.ts b/src/hooks.ts index a84b266..cec61a0 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -270,9 +270,9 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri const isClientReady = isServerSide || !!optimizely?.isReady(); const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled(); - + const canMakeDecision = optimizely?.getOptimizelyConfig() && optimizely.getUserContext(); const [state, setState] = useState(() => { - const decisionState = isClientReady ? getCurrentDecision() : { variation: null }; + const decisionState = canMakeDecision ? getCurrentDecision() : { variation: null }; return { ...decisionState, clientReady: isClientReady, @@ -368,9 +368,10 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) const isClientReady = isServerSide || !!optimizely?.isReady(); const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled(); + const canMakeDecision = optimizely?.getOptimizelyConfig() && optimizely.getUserContext(); const [state, setState] = useState(() => { - const decisionState = isClientReady ? getCurrentDecision() : { isEnabled: false, variables: {} }; + const decisionState = canMakeDecision ? getCurrentDecision() : { isEnabled: false, variables: {} }; return { ...decisionState, clientReady: isClientReady, @@ -467,9 +468,10 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) const isClientReady = isServerSide || !!optimizely?.isReady(); const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled(); + const canMakeDecision = optimizely?.getOptimizelyConfig() && optimizely.getUserContext(); const [state, setState] = useState<{ decision: OptimizelyDecision } & InitializationState>(() => { - const decisionState = isClientReady + const decisionState = canMakeDecision ? getCurrentDecision() : { decision: defaultDecision, From 7c2cb07a03d6b550b06b678794463655a1d67e30 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:24:46 +0600 Subject: [PATCH 03/13] [FSSDK-10777] ssr support update --- README.md | 120 ++++++++++-------------- docs/nextjs-ssr.md | 222 +++++++++++++++++++++++++++++++++++++++++++++ src/hooks.spec.tsx | 41 +++++++++ 3 files changed, 310 insertions(+), 73 deletions(-) create mode 100644 docs/nextjs-ssr.md diff --git a/README.md b/README.md index 9cd42c5..461d982 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) @@ -155,9 +151,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 +273,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 +294,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 +319,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 +330,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 ; } ``` @@ -411,69 +408,46 @@ 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). To generate synchronous decisions during SSR, you must pre-fetch the datafile and pass it to `createInstance`. Using `sdkKey` alone is not supported for SSR because it requires an asynchronous network call. -**Caveats** +### Setup -1. You must download the datafile manually and pass in via the `datafile` option. Can not use `sdkKey` to automatically download. +Fetch the datafile on the server, create an Optimizely instance, and wrap your app with ``: -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. +```jsx +import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/react-sdk'; -### Setting up `` +// Pre-fetched datafile (fetching mechanism depends on your framework) +const optimizelyClient = createInstance({ + datafile, // must be provided for SSR +}); -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. +function MyComponent() { + const [decision] = useDecision('flag1'); + return decision.enabled ?

Feature enabled

:

Feature disabled

; +} -```jsx - - - +// Wrap your app with OptimizelyProvider + + +; ``` -All other Optimizely components, such as `` and `` can remain the same. +### React Server Components -### Full example +The SDK can also be used directly in React Server Components without `OptimizelyProvider`. See the [Next.js Integration Guide](docs/nextjs-ssr.md#react-server-components) for details. -```jsx -import * as React from 'react'; -import * as ReactDOMServer from 'react-dom/server'; +### Next.js Integration -import { - createInstance, - OptimizelyProvider, - useDecision, -} from '@optimizely/react-sdk'; +For detailed Next.js examples covering both App Router and Pages Router patterns, see the [Next.js Integration Guide](docs/nextjs-ssr.md). -const fetch = require('node-fetch'); +### Limitations -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

} -
- ); -} +- **Datafile required** — SSR requires a pre-fetched datafile. Using `sdkKey` alone falls back to a failed decision. +- **Static user only** — User `Promise` is not supported during SSR. +- **ODP segments unavailable** — ODP audience segments require async I/O and are not available during server rendering. -async function main() { - const resp = await fetch('https://cdn.optimizely.com/datafiles/.json'); - const datafile = await resp.json(); - const optimizelyClient = createInstance({ - datafile, - }); - - const output = ReactDOMServer.renderToString( - - - - ); - console.log('output', output); -} -main(); -``` +For more details and workarounds, see the [Next.js Integration Guide — Limitations](docs/nextjs-ssr.md#limitations). ## Disabled event dispatcher diff --git a/docs/nextjs-ssr.md b/docs/nextjs-ssr.md new file mode 100644 index 0000000..e92b4c3 --- /dev/null +++ b/docs/nextjs-ssr.md @@ -0,0 +1,222 @@ +# Next.js Integration Guide + +This guide covers how to use the Optimizely React SDK with Next.js for server-side rendering (SSR) 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 many 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 + +```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 } from '@optimizely/react-sdk'; +import { ReactNode, useState } from 'react'; + +export function OptimizelyClientProvider({ children, datafile }: { children: ReactNode; datafile: object }) { + const [optimizely] = useState(() => createInstance({ datafile })); + const isServerSide = typeof window === 'undefined'; + + return ( + + {children} + + ); +} +``` + +### 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} + + + ); +} +``` + +## Next.js Pages Router + +In the Pages Router, fetch the datafile in `getServerSideProps` (or `getStaticProps`) and pass it through `_app.tsx`. + +### 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. Set up `_app.tsx` + +```tsx +// pages/_app.tsx +import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider'; +import type { AppProps } from 'next/app'; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + ); +} +``` + +### 3. Fetch the datafile in your page + +```tsx +// pages/index.tsx +export async function getServerSideProps() { + const res = await fetch(`https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`); + const datafile = await res.json(); + + return { props: { datafile } }; +} +``` + +#### Alternative: Static generation with revalidation + +If you prefer build-time fetching with periodic revalidation instead of per-request fetching: + +```tsx +export async function getStaticProps() { + const res = await fetch(`https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`); + const datafile = await res.json(); + + 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, isClientReady, didTimeout] = useDecision('banner-flag'); + + if (!didTimeout && !isClientReady) { + return

Loading...

; + } + + return decision.enabled ?

New Banner

:

Default Banner

; +} +``` + +## React Server Components + +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 +// src/app/components/ServerExperiment.tsx +import { createInstance } from '@optimizely/react-sdk'; + +export default async function ServerExperiment() { + const client = createInstance({ + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', + }); + + client.setUser({ + id: 'user-123', + }); + + await client.onReady(); + + const decision = client.decide('flag-1'); + + return decision.enabled ?

Experiment Variation

:

Control

; +} +``` + +> **Note:** Server Components render only on the server and do not ship JavaScript to the client. This means there is no client-side interactivity or auto-updating of decisions. For interactive feature flags, use a client component with `useDecision` instead. + +## 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

; +} +``` + +### Static user only + +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 unavailable + +ODP (Optimizely Data Platform) audience segments require fetching segment data via an async network call, which is not available during server rendering. Decisions will be made without audience segment data. If your experiment relies on ODP segments, consider using the loading state pattern above and deferring the decision to the client. diff --git a/src/hooks.spec.tsx b/src/hooks.spec.tsx index aa8c344..865dd01 100644 --- a/src/hooks.spec.tsx +++ b/src/hooks.spec.tsx @@ -1020,6 +1020,47 @@ describe('hooks', () => { await waitFor(() => expect(mockLog).toHaveBeenCalledWith(true)); }); + it('should re-render with updated decision after fetchQualifiedSegments completes via setUser', async () => { + // Simulate ODP scenario: config + userContext available synchronously (canMakeDecision = true), + // but client not fully ready yet (fetchQualifiedSegments still pending) + (optimizelyMock.getOptimizelyConfig as jest.Mock).mockReturnValue({}); + (optimizelyMock.getUserContext as jest.Mock).mockReturnValue({}); + (optimizelyMock.isReady as any) = () => false; + (optimizelyMock.getIsReadyPromiseFulfilled as any) = () => false; + + // Phase 1: decision without ODP segments + decideMock.mockReturnValue({ ...defaultDecision, enabled: false }); + + let resolveReadyPromise: (result: { success: boolean }) => void; + const readyPromise: Promise = new Promise((res) => { + resolveReadyPromise = res; + }); + getOnReadyPromise = (): Promise => readyPromise; + + render( + + + + ); + + // Phase 1: canMakeDecision is true, so hook evaluates sync decision (without segments) + expect(mockLog).toHaveBeenCalledTimes(1); + expect(mockLog).toHaveBeenCalledWith(false); + + mockLog.mockReset(); + + // Phase 2: fetchQualifiedSegments completes, setUser resolves userPromise, onReady resolves + // Now decision includes segment-based targeting + decideMock.mockReturnValue({ ...defaultDecision, enabled: true }); + + await act(async () => { + resolveReadyPromise!({ success: true }); + }); + + await waitFor(() => expect(mockLog).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(mockLog).toHaveBeenCalledWith(true)); + }); + it('should re-render after updating the override user ID argument', async () => { decideMock.mockReturnValue({ ...defaultDecision }); const { rerender } = render( From 68923cf2c30e328a0d5e479bcbd0d85e86b4b93c Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:33:06 +0600 Subject: [PATCH 04/13] [FSSDK-10777] doc improvement --- README.md | 24 +++++++++++++++++++++++- docs/nextjs-ssr.md | 27 --------------------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 461d982..0aa3f96 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,29 @@ function MyComponent() { ### React Server Components -The SDK can also be used directly in React Server Components without `OptimizelyProvider`. See the [Next.js Integration Guide](docs/nextjs-ssr.md#react-server-components) for details. +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: + +```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'); + + return decision.enabled + ?

Experiment Variation

+ :

Control

; +} +``` ### Next.js Integration diff --git a/docs/nextjs-ssr.md b/docs/nextjs-ssr.md index e92b4c3..c5600bc 100644 --- a/docs/nextjs-ssr.md +++ b/docs/nextjs-ssr.md @@ -154,33 +154,6 @@ export default function FeatureBanner() { } ``` -## React Server Components - -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 -// src/app/components/ServerExperiment.tsx -import { createInstance } from '@optimizely/react-sdk'; - -export default async function ServerExperiment() { - const client = createInstance({ - sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', - }); - - client.setUser({ - id: 'user-123', - }); - - await client.onReady(); - - const decision = client.decide('flag-1'); - - return decision.enabled ?

Experiment Variation

:

Control

; -} -``` - -> **Note:** Server Components render only on the server and do not ship JavaScript to the client. This means there is no client-side interactivity or auto-updating of decisions. For interactive feature flags, use a client component with `useDecision` instead. - ## Limitations ### Datafile required for SSR From 2af637d74c0a63bd5d6eb99c1946e260d33843a6 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:48:36 +0600 Subject: [PATCH 05/13] [FSSDK-10777] doc improvement --- README.md | 26 ++++++++++++++++++++++++++ docs/nextjs-ssr.md | 16 ++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0aa3f96..d526af6 100644 --- a/README.md +++ b/README.md @@ -433,6 +433,32 @@ function MyComponent() {
; ``` +### Configuring the instance for server use + +Server-side instances are short-lived (created per request) and may not be garbage collected immediately. To avoid unnecessary background work and ensure events are dispatched before the instance is discarded, configure `createInstance` with server-appropriate options: + +```jsx +import { createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk'; + +const isServer = typeof window === 'undefined'; + +const optimizelyClient = createInstance({ + datafile, + datafileOptions: { autoUpdate: !isServer }, + eventBatchSize: isServer ? 1 : 10, + eventMaxQueueSize: isServer ? 1 : 100, + // Optional: disable decision events on server if they will be sent from the client + defaultDecideOptions: isServer ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], +}); +``` + +| Option | Server value | Why | +|---|---|---| +| `datafileOptions.autoUpdate` | `false` | No need to poll for datafile updates on a per-request instance | +| `eventBatchSize` | `1` | Flush events immediately — the instance won't live long enough for a batch to fill | +| `eventMaxQueueSize` | `1` | Prevent event accumulation in a short-lived instance | +| `defaultDecideOptions` | `[DISABLE_DECISION_EVENT]` | Optional — avoids duplicate decision events if the client will also fire them after hydration | + ### React Server Components 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: diff --git a/docs/nextjs-ssr.md b/docs/nextjs-ssr.md index c5600bc..87fd200 100644 --- a/docs/nextjs-ssr.md +++ b/docs/nextjs-ssr.md @@ -47,11 +47,21 @@ Since `OptimizelyProvider` uses React Context (a client-side feature), it must b // src/providers/OptimizelyProvider.tsx 'use client'; -import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; +import { OptimizelyProvider, createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk'; import { ReactNode, useState } from 'react'; export function OptimizelyClientProvider({ children, datafile }: { children: ReactNode; datafile: object }) { - const [optimizely] = useState(() => createInstance({ datafile })); + const [optimizely] = useState(() => { + const isServer = typeof window === 'undefined'; + return createInstance({ + datafile, + datafileOptions: { autoUpdate: !isServer }, + eventBatchSize: isServer ? 1 : 10, + eventMaxQueueSize: isServer ? 1 : 100, + // Optional: disable decision events on server if they will be sent from the client + defaultDecideOptions: isServer ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], + }); + }); const isServerSide = typeof window === 'undefined'; return ( @@ -62,6 +72,8 @@ export function OptimizelyClientProvider({ children, datafile }: { children: Rea } ``` +> 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 From 40c71cc49706ed2b974c38ab26b3a24645337915 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:53:32 +0600 Subject: [PATCH 06/13] [FSSDK-10777] doc improvement --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d526af6..e899715 100644 --- a/README.md +++ b/README.md @@ -479,6 +479,8 @@ export default async function ServerExperiment() { const decision = client.decide('flag-1'); + client.close(); + return decision.enabled ?

Experiment Variation

:

Control

; From be511d36b5bd2b16966dab2b492d987e6aac00ab Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:00:19 +0600 Subject: [PATCH 07/13] [FSSDK-10777] improvement --- src/hooks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index cec61a0..0fc17ed 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -270,7 +270,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri const isClientReady = isServerSide || !!optimizely?.isReady(); const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled(); - const canMakeDecision = optimizely?.getOptimizelyConfig() && optimizely.getUserContext(); + const canMakeDecision = !!(optimizely?.getOptimizelyConfig() && optimizely.getUserContext()); const [state, setState] = useState(() => { const decisionState = canMakeDecision ? getCurrentDecision() : { variation: null }; return { @@ -368,7 +368,7 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) const isClientReady = isServerSide || !!optimizely?.isReady(); const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled(); - const canMakeDecision = optimizely?.getOptimizelyConfig() && optimizely.getUserContext(); + const canMakeDecision = !!(optimizely?.getOptimizelyConfig() && optimizely.getUserContext()); const [state, setState] = useState(() => { const decisionState = canMakeDecision ? getCurrentDecision() : { isEnabled: false, variables: {} }; @@ -468,7 +468,7 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) const isClientReady = isServerSide || !!optimizely?.isReady(); const isReadyPromiseFulfilled = !!optimizely?.getIsReadyPromiseFulfilled(); - const canMakeDecision = optimizely?.getOptimizelyConfig() && optimizely.getUserContext(); + const canMakeDecision = !!(optimizely?.getOptimizelyConfig() && optimizely.getUserContext()); const [state, setState] = useState<{ decision: OptimizelyDecision } & InitializationState>(() => { const decisionState = canMakeDecision From 252515f8ba4d3ed1be120586c2dda53cc4c25549 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:04:30 +0600 Subject: [PATCH 08/13] [FSSDK-10777] doc improvement --- README.md | 2 ++ docs/nextjs-ssr.md | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e899715..5edf91b 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,7 @@ import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/rea // Pre-fetched datafile (fetching mechanism depends on your framework) const optimizelyClient = createInstance({ datafile, // must be provided for SSR + sdkKey: 'YOUR_SDK_KEY' }); function MyComponent() { @@ -444,6 +445,7 @@ const isServer = typeof window === 'undefined'; const optimizelyClient = createInstance({ datafile, + sdkKey: 'YOUR_SDK_KEY' datafileOptions: { autoUpdate: !isServer }, eventBatchSize: isServer ? 1 : 10, eventMaxQueueSize: isServer ? 1 : 100, diff --git a/docs/nextjs-ssr.md b/docs/nextjs-ssr.md index 87fd200..b06f960 100644 --- a/docs/nextjs-ssr.md +++ b/docs/nextjs-ssr.md @@ -156,12 +156,8 @@ Once the provider is set up, use the `useDecision` hook in any client component: import { useDecision } from '@optimizely/react-sdk'; export default function FeatureBanner() { - const [decision, isClientReady, didTimeout] = useDecision('banner-flag'); - - if (!didTimeout && !isClientReady) { - return

Loading...

; - } - + const [decision] = useDecision('banner-flag'); + return decision.enabled ?

New Banner

:

Default Banner

; } ``` From 05a4a2ec92779fc3701d7f2ed712db0b746c6b28 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:07:53 +0600 Subject: [PATCH 09/13] [FSSDK-10777] doc improvement --- README.md | 4 ++-- docs/{nextjs-ssr.md => nextjs-integration.md} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/{nextjs-ssr.md => nextjs-integration.md} (100%) diff --git a/README.md b/README.md index 5edf91b..1aba445 100644 --- a/README.md +++ b/README.md @@ -491,7 +491,7 @@ export default async function ServerExperiment() { ### Next.js Integration -For detailed Next.js examples covering both App Router and Pages Router patterns, see the [Next.js Integration Guide](docs/nextjs-ssr.md). +For detailed Next.js examples covering both App Router and Pages Router patterns, see the [Next.js Integration Guide](docs/nextjs-integration.md). ### Limitations @@ -499,7 +499,7 @@ For detailed Next.js examples covering both App Router and Pages Router patterns - **Static user only** — User `Promise` is not supported during SSR. - **ODP segments unavailable** — ODP audience segments require async I/O and are not available during server rendering. -For more details and workarounds, see the [Next.js Integration Guide — Limitations](docs/nextjs-ssr.md#limitations). +For more details and workarounds, see the [Next.js Integration Guide — Limitations](docs/nextjs-integration.md#limitations). ## Disabled event dispatcher diff --git a/docs/nextjs-ssr.md b/docs/nextjs-integration.md similarity index 100% rename from docs/nextjs-ssr.md rename to docs/nextjs-integration.md From 892fca63125eb3f924036dc18c4893b5a021f36f Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:16:23 +0600 Subject: [PATCH 10/13] [FSSDK-10777] ssg doc update --- docs/nextjs-integration.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/nextjs-integration.md b/docs/nextjs-integration.md index b06f960..950d157 100644 --- a/docs/nextjs-integration.md +++ b/docs/nextjs-integration.md @@ -1,6 +1,6 @@ # Next.js Integration Guide -This guide covers how to use the Optimizely React SDK with Next.js for server-side rendering (SSR) and React Server Components. +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 @@ -162,6 +162,36 @@ export default function FeatureBanner() { } ``` +## 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 From c28e1c87418859386c203ec575a71c68df95da57 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:21:25 +0600 Subject: [PATCH 11/13] [FSSDK-10777] server bundle addition --- README.md | 68 +++++++++++------------------ docs/nextjs-integration.md | 86 +++++++++++++++++++++++++++---------- package.json | 14 ++++++ scripts/build.js | 13 +++++- scripts/config.js | 5 ++- src/reactUtils.tsx | 39 +++++++++++++++++ src/server.ts | 42 ++++++++++++++++++ src/utils.spec.tsx | 5 ++- src/{utils.tsx => utils.ts} | 25 +---------- src/withOptimizely.tsx | 4 +- 10 files changed, 205 insertions(+), 96 deletions(-) create mode 100644 src/reactUtils.tsx create mode 100644 src/server.ts rename src/{utils.tsx => utils.ts} (73%) diff --git a/README.md b/README.md index 1aba445..f3b90fc 100644 --- a/README.md +++ b/README.md @@ -408,58 +408,42 @@ To rollout or experiment on a feature by user rather than by random percentage, ## Server Side Rendering -The React SDK supports server-side rendering (SSR). To generate synchronous decisions during SSR, you must pre-fetch the datafile and pass it to `createInstance`. Using `sdkKey` alone is not supported for SSR because it requires an asynchronous network call. - -### Setup - -Fetch the datafile on the server, create an Optimizely instance, and wrap your app with ``: +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, useDecision } from '@optimizely/react-sdk'; - -// Pre-fetched datafile (fetching mechanism depends on your framework) -const optimizelyClient = createInstance({ - datafile, // must be provided for SSR - sdkKey: 'YOUR_SDK_KEY' -}); +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

; } -// Wrap your app with OptimizelyProvider - + ; ``` -### Configuring the instance for server use - -Server-side instances are short-lived (created per request) and may not be garbage collected immediately. To avoid unnecessary background work and ensure events are dispatched before the instance is discarded, configure `createInstance` with server-appropriate options: - -```jsx -import { createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk'; - -const isServer = typeof window === 'undefined'; - -const optimizelyClient = createInstance({ - datafile, - sdkKey: 'YOUR_SDK_KEY' - datafileOptions: { autoUpdate: !isServer }, - eventBatchSize: isServer ? 1 : 10, - eventMaxQueueSize: isServer ? 1 : 100, - // Optional: disable decision events on server if they will be sent from the client - defaultDecideOptions: isServer ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], -}); -``` - -| Option | Server value | Why | -|---|---|---| -| `datafileOptions.autoUpdate` | `false` | No need to poll for datafile updates on a per-request instance | -| `eventBatchSize` | `1` | Flush events immediately — the instance won't live long enough for a batch to fill | -| `eventMaxQueueSize` | `1` | Prevent event accumulation in a short-lived instance | -| `defaultDecideOptions` | `[DISABLE_DECISION_EVENT]` | Optional — avoids duplicate decision events if the client will also fire them after hydration | +| 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 | ### React Server Components @@ -483,9 +467,7 @@ export default async function ServerExperiment() { client.close(); - return decision.enabled - ?

Experiment Variation

- :

Control

; + return decision.enabled ?

Experiment Variation

:

Control

; } ``` diff --git a/docs/nextjs-integration.md b/docs/nextjs-integration.md index 950d157..5a7b403 100644 --- a/docs/nextjs-integration.md +++ b/docs/nextjs-integration.md @@ -24,6 +24,33 @@ In the App Router, fetch the datafile in an async server component (e.g., your r ### 1. Create a datafile fetcher +There are several ways to fetch the datafile. Here are two common approaches: + +**Option A: Using the SDK's built-in datafile fetching (Recommended)** + +Create an SDK instance with your `sdkKey` and let it fetch and cache the datafile internally. 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 || "", +}); + +export async function getDatafile() { + pollingInstance.setUser({ + id: "dummyUser", + }); + await pollingInstance.onReady(); + return pollingInstance.getOptimizelyConfig()?.getDatafile(); +} +``` + +**Option B: Direct CDN fetch** + +Fetch the datafile directly from Optimizely's CDN. This is simpler and gives you full control over caching (e.g., via Next.js `fetch` options like `next.revalidate`), but does not include automatic polling for updates. + ```ts // src/data/getDatafile.ts const CDN_URL = `https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`; @@ -51,18 +78,19 @@ import { OptimizelyProvider, createInstance, OptimizelyDecideOption } from '@opt import { ReactNode, useState } from 'react'; export function OptimizelyClientProvider({ children, datafile }: { children: ReactNode; datafile: object }) { - const [optimizely] = useState(() => { - const isServer = typeof window === 'undefined'; - return createInstance({ + const isServerSide = typeof window === 'undefined'; + + const [optimizely] = useState(() => + createInstance({ datafile, - datafileOptions: { autoUpdate: !isServer }, - eventBatchSize: isServer ? 1 : 10, - eventMaxQueueSize: isServer ? 1 : 100, - // Optional: disable decision events on server if they will be sent from the client - defaultDecideOptions: isServer ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], - }); - }); - const isServerSide = typeof window === 'undefined'; + sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '', + datafileOptions: { autoUpdate: !isServerSide }, + defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], + odpOptions: { + disabled: isServerSide, + }, + }) + ); return ( @@ -96,18 +124,25 @@ export default async function RootLayout({ children }: { children: React.ReactNo ## Next.js Pages Router -In the Pages Router, fetch the datafile in `getServerSideProps` (or `getStaticProps`) and pass it through `_app.tsx`. +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. Set up `_app.tsx` +### 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 } from 'next/app'; +import type { AppProps, AppContext } from 'next/app'; +import { getDatafile } from '@/data/getDatafile'; export default function App({ Component, pageProps }: AppProps) { return ( @@ -116,28 +151,35 @@ export default function App({ Component, pageProps }: AppProps) { ); } + +App.getInitialProps = async (appContext: AppContext) => { + const appProps = await App.getInitialProps(appContext); + const datafile = await getDatafile(); + return { ...appProps, pageProps: { ...appProps.pageProps, datafile } }; +}; ``` -### 3. Fetch the datafile in your page +#### 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 res = await fetch(`https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`); - const datafile = await res.json(); + const datafile = await getDatafile(); return { props: { datafile } }; } ``` -#### Alternative: Static generation with revalidation +#### Option C: `getStaticProps` — static generation with revalidation -If you prefer build-time fetching with periodic revalidation instead of per-request fetching: +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 res = await fetch(`https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`); - const datafile = await res.json(); + const datafile = await getDatafile(); return { props: { datafile }, @@ -216,7 +258,7 @@ export default function MyFeature() { } ``` -### Static user only +### User Promise not supported User `Promise` is not supported during SSR. You must provide a static user object to `OptimizelyProvider`: diff --git a/package.json b/package.json index 303393d..1450232 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,20 @@ "types": "dist/index.d.ts", "main": "dist/react-sdk.js", "browser": "dist/react-sdk.js", + "exports": { + ".": { + "react-server": { + "types": "./dist/server.d.ts", + "import": "./dist/server.es.js", + "require": "./dist/server.js", + "default": "./dist/server.js" + }, + "types": "./dist/index.d.ts", + "import": "./dist/react-sdk.es.js", + "require": "./dist/react-sdk.js", + "default": "./dist/react-sdk.js" + } + }, "directories": { "lib": "lib" }, diff --git a/scripts/build.js b/scripts/build.js index 11e6859..d658bcd 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,5 +1,5 @@ /** - * Copyright 2019, 2023 Optimizely + * Copyright 2019, 2023, 2026 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ +/* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); const execSync = require('child_process').execSync; @@ -46,3 +47,13 @@ exec(`./node_modules/.bin/rollup -c scripts/config.js -f system -o dist/${packag EXTERNALS: 'forBrowsers', BUILD_ENV: 'production', }); + +console.log('\nBuilding server ES modules...'); +exec(`./node_modules/.bin/rollup -c scripts/config.js -f es -o dist/server.es.js`, { + ENTRY: 'src/server.ts', +}); + +console.log('\nBuilding server CommonJS modules...'); +exec(`./node_modules/.bin/rollup -c scripts/config.js -f cjs -o dist/server.js`, { + ENTRY: 'src/server.ts', +}); diff --git a/scripts/config.js b/scripts/config.js index f939b65..4098a56 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -1,5 +1,5 @@ /** - * Copyright 2019, 2023 Optimizely + * Copyright 2019, 2023, 2026 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ +/* eslint-disable @typescript-eslint/no-var-requires */ const typescript = require('rollup-plugin-typescript2'); const commonjs = require('@rollup/plugin-commonjs'); const replace = require('@rollup/plugin-replace'); @@ -65,7 +66,7 @@ function getPlugins(env, externals) { } const config = { - input: 'src/index.ts', + input: process.env.ENTRY || 'src/index.ts', output: { globals: { react: 'React', diff --git a/src/reactUtils.tsx b/src/reactUtils.tsx new file mode 100644 index 0000000..3deaf0f --- /dev/null +++ b/src/reactUtils.tsx @@ -0,0 +1,39 @@ +/** + * Copyright 2026 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import hoistNonReactStatics from 'hoist-non-react-statics'; +import * as React from 'react'; + +export interface AcceptsForwardedRef { + forwardedRef?: React.Ref; +} + +export function hoistStaticsAndForwardRefs>( + Target: React.ComponentType

, + Source: React.ComponentType, + displayName: string +): React.ForwardRefExoticComponent & React.RefAttributes> { + // Make sure to hoist statics and forward any refs through from Source to Target + // From the React docs: + // https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over + // https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-in-higher-order-components + const forwardRef: React.ForwardRefRenderFunction = (props, ref) => ; + forwardRef.displayName = `${displayName}(${Source.displayName || Source.name})`; + return hoistNonReactStatics< + React.ForwardRefExoticComponent & React.RefAttributes>, + React.ComponentType + >(React.forwardRef(forwardRef), Source); +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..08943b2 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2026 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Server-safe entry point for @optimizely/react-sdk. + * + * This module can be safely imported in React Server Components (RSC) + * as it does not use any client-only React APIs (createContext, hooks, etc.). + */ + +export { createInstance, ReactSDKClient } from './client'; + +export { OptimizelyDecision } from './utils'; + +export { default as logOnlyEventDispatcher } from './logOnlyEventDispatcher'; + +export { + logging, + errorHandler, + setLogger, + setLogLevel, + enums, + eventDispatcher, + OptimizelyDecideOption, + ActivateListenerPayload, + TrackListenerPayload, + ListenerPayload, + OptimizelySegmentOption, +} from '@optimizely/optimizely-sdk'; diff --git a/src/utils.spec.tsx b/src/utils.spec.tsx index 07cad2f..bf27636 100644 --- a/src/utils.spec.tsx +++ b/src/utils.spec.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2024 Optimizely + * Copyright 2024, 2026 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ * limitations under the License. */ import * as utils from './utils'; +import * as reactUtils from './reactUtils'; import React, { forwardRef } from 'react'; import { render, screen } from '@testing-library/react'; import hoistNonReactStatics from 'hoist-non-react-statics'; @@ -74,7 +75,7 @@ describe('utils', () => { } } - const WrappedComponent = utils.hoistStaticsAndForwardRefs(TestComponent, SourceComponent, 'WrappedComponent'); + const WrappedComponent = reactUtils.hoistStaticsAndForwardRefs(TestComponent, SourceComponent, 'WrappedComponent'); it('should forward refs and hoist static methods', () => { const ref = React.createRef(); diff --git a/src/utils.tsx b/src/utils.ts similarity index 73% rename from src/utils.tsx rename to src/utils.ts index b1f35bc..345204a 100644 --- a/src/utils.tsx +++ b/src/utils.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019, Optimizely + * Copyright 2026 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,7 @@ * limitations under the License. */ -import hoistNonReactStatics from 'hoist-non-react-statics'; import * as optimizely from '@optimizely/optimizely-sdk'; -import * as React from 'react'; export type UserInfo = { id: string | null; @@ -51,27 +49,6 @@ export function areUsersEqual(user1: UserInfo, user2: UserInfo): boolean { return true; } -export interface AcceptsForwardedRef { - forwardedRef?: React.Ref; -} - -export function hoistStaticsAndForwardRefs>( - Target: React.ComponentType

, - Source: React.ComponentType, - displayName: string -): React.ForwardRefExoticComponent & React.RefAttributes> { - // Make sure to hoist statics and forward any refs through from Source to Target - // From the React docs: - // https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over - // https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-in-higher-order-components - const forwardRef: React.ForwardRefRenderFunction = (props, ref) => ; - forwardRef.displayName = `${displayName}(${Source.displayName || Source.name})`; - return hoistNonReactStatics< - React.ForwardRefExoticComponent & React.RefAttributes>, - React.ComponentType - >(React.forwardRef(forwardRef), Source); -} - function coerceUnknownAttrsValueForComparison(maybeAttrs: unknown): optimizely.UserAttributes { if (typeof maybeAttrs === 'object' && maybeAttrs !== null) { return maybeAttrs as optimizely.UserAttributes; diff --git a/src/withOptimizely.tsx b/src/withOptimizely.tsx index 0160f17..b822bc2 100644 --- a/src/withOptimizely.tsx +++ b/src/withOptimizely.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2018-2019, Optimizely + * Copyright 2018-2019, 2026 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import * as React from 'react'; import { OptimizelyContextConsumer, OptimizelyContextInterface } from './Context'; import { ReactSDKClient } from './client'; -import { hoistStaticsAndForwardRefs } from './utils'; +import { hoistStaticsAndForwardRefs } from './reactUtils'; export interface WithOptimizelyProps { optimizely: ReactSDKClient | null; From 441bf833029241cdb6d4b87d4927afcb450eed55 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Tue, 3 Mar 2026 20:46:23 +0600 Subject: [PATCH 12/13] [FSSDK-10777] segments support addition --- src/Provider.spec.tsx | 47 ++++++++++++++----------- src/Provider.tsx | 11 +++--- src/client.spec.ts | 68 +++++++++++++++++++++++++++++++++++++ src/client.ts | 7 ++-- src/hooks.spec.tsx | 19 +++++++++++ src/withOptimizely.spec.tsx | 46 +++++++++++++++---------- 6 files changed, 155 insertions(+), 43 deletions(-) diff --git a/src/Provider.spec.tsx b/src/Provider.spec.tsx index 4d89012..fb62449 100644 --- a/src/Provider.spec.tsx +++ b/src/Provider.spec.tsx @@ -58,13 +58,13 @@ describe('OptimizelyProvider', () => { it('should resolve user promise and set user in optimizely', async () => { render(); - await waitFor(() => expect(mockReactClient.setUser).toHaveBeenCalledWith(user1)); + await waitFor(() => expect(mockReactClient.setUser).toHaveBeenCalledWith(user1, undefined)); }); it('should render successfully with user provided', () => { render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith(user1); + expect(mockReactClient.setUser).toHaveBeenCalledWith(user1, undefined); }); it('should throw error, if setUser throws error', () => { @@ -76,10 +76,13 @@ describe('OptimizelyProvider', () => { it('should render successfully with userId provided', () => { render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith({ - id: user1.id, - attributes: {}, - }); + expect(mockReactClient.setUser).toHaveBeenCalledWith( + { + id: user1.id, + attributes: {}, + }, + undefined + ); }); it('should render successfully without user or userId provided', () => { @@ -87,13 +90,13 @@ describe('OptimizelyProvider', () => { mockReactClient.user = undefined; render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith(DefaultUser); + expect(mockReactClient.setUser).toHaveBeenCalledWith(DefaultUser, undefined); }); it('should render successfully with user id & attributes provided', () => { render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith(user1); + expect(mockReactClient.setUser).toHaveBeenCalledWith(user1, undefined); }); it('should succeed just userAttributes provided', () => { @@ -101,25 +104,31 @@ describe('OptimizelyProvider', () => { mockReactClient.user = undefined; render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith({ - id: DefaultUser.id, - attributes: { attr1: 'value1' }, - }); + expect(mockReactClient.setUser).toHaveBeenCalledWith( + { + id: DefaultUser.id, + attributes: { attr1: 'value1' }, + }, + undefined + ); }); it('should succeed with the initial user available in client', () => { render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith(user1); + expect(mockReactClient.setUser).toHaveBeenCalledWith(user1, undefined); }); it('should succeed with the initial user id and newly passed attributes', () => { render(); - expect(mockReactClient.setUser).toHaveBeenCalledWith({ - id: user1.id, - attributes: { attr1: 'value2' }, - }); + expect(mockReactClient.setUser).toHaveBeenCalledWith( + { + id: user1.id, + attributes: { attr1: 'value2' }, + }, + undefined + ); }); it('should not update when isServerSide is true', () => { @@ -142,7 +151,7 @@ describe('OptimizelyProvider', () => { // Change props to trigger componentDidUpdate rerender(); - expect(mockReactClient.setUser).toHaveBeenCalledWith(user1); + expect(mockReactClient.setUser).toHaveBeenCalledWith(user1, undefined); }); it('should update user if users are not equal', () => { @@ -153,7 +162,7 @@ describe('OptimizelyProvider', () => { // Change props to a different user to trigger componentDidUpdate rerender(); - expect(mockReactClient.setUser).toHaveBeenCalledWith(user2); + expect(mockReactClient.setUser).toHaveBeenCalledWith(user2, undefined); }); it('should not update user if users are equal', () => { diff --git a/src/Provider.tsx b/src/Provider.tsx index 5d4a134..f0f047f 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -30,6 +30,7 @@ interface OptimizelyProviderProps { user?: Promise | UserInfo; userId?: string; userAttributes?: UserAttributes; + qualifiedSegments?: string[]; children?: React.ReactNode; } @@ -46,7 +47,7 @@ export class OptimizelyProvider extends React.Component { - const { optimizely, userId, userAttributes, user } = this.props; + const { optimizely, userId, userAttributes, user, qualifiedSegments } = this.props; if (!optimizely) { logger.error('OptimizelyProvider must be passed an instance of the Optimizely SDK client'); @@ -58,7 +59,7 @@ export class OptimizelyProvider extends React.Component { - optimizely.setUser(res); + optimizely.setUser(res, qualifiedSegments); }); } else { finalUser = { @@ -89,7 +90,7 @@ export class OptimizelyProvider extends React.Component { expect(instance.fetchQualifiedSegments).toHaveBeenCalledTimes(3); }); + + it('should set qualifiedSegments synchronously on userContext before fetchQualifiedSegments is called', async () => { + jest.spyOn(mockInnerClient, 'isOdpIntegrated').mockReturnValue(true); + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); + jest.spyOn(mockOptimizelyUserContext, 'getAttributes').mockReturnValue(userAttributes); + + const segments = ['segment1', 'segment2']; + let segmentsAtFetchTime: string[] | null | undefined; + + instance = createInstance(config); + jest.spyOn(instance, 'fetchQualifiedSegments').mockImplementation(async () => { + // Capture qualifiedSegments at the time fetchQualifiedSegments is called + segmentsAtFetchTime = mockOptimizelyUserContext.qualifiedSegments; + return true; + }); + + await instance.setUser({ id: userId, attributes: userAttributes }, segments); + + // Verify segments were already set on the userContext before fetchQualifiedSegments ran + expect(segmentsAtFetchTime).toEqual(segments); + }); + + it('should not set qualifiedSegments when ODP is explicitly disabled', async () => { + jest.spyOn(mockInnerClient, 'isOdpIntegrated').mockReturnValue(true); + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); + jest.spyOn(mockOptimizelyUserContext, 'getAttributes').mockReturnValue(userAttributes); + + const segments = ['segment1', 'segment2']; + + instance = createInstance({ + ...config, + odpOptions: { disabled: true }, + }); + jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(true); + + await instance.setUser({ id: userId, attributes: userAttributes }, segments); + + expect(mockOptimizelyUserContext.qualifiedSegments).toBeUndefined(); + }); + + it('should not set qualifiedSegments when ODP is not integrated', async () => { + jest.spyOn(mockInnerClient, 'isOdpIntegrated').mockReturnValue(false); + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); + jest.spyOn(mockOptimizelyUserContext, 'getAttributes').mockReturnValue(userAttributes); + + const segments = ['segment1', 'segment2']; + + instance = createInstance(config); + jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(true); + + await instance.setUser({ id: userId, attributes: userAttributes }, segments); + + expect(mockOptimizelyUserContext.qualifiedSegments).toBeUndefined(); + }); + + it('should not set qualifiedSegments for anonymous/default users', async () => { + jest.spyOn(mockInnerClient, 'isOdpIntegrated').mockReturnValue(true); + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(validVuid); + + const segments = ['segment1', 'segment2']; + + instance = createInstance(config); + jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(true); + + await instance.setUser(DefaultUser, segments); + + expect(mockOptimizelyUserContext.qualifiedSegments).toBeUndefined(); + }); }); describe('onUserUpdate', () => { diff --git a/src/client.ts b/src/client.ts index cfc2115..566dd66 100644 --- a/src/client.ts +++ b/src/client.ts @@ -59,7 +59,7 @@ export interface ReactSDKClient user: UserInfo; client: optimizely.Client | null; onReady(opts?: { timeout?: number }): Promise; - setUser(userInfo: UserInfo): Promise; + setUser(userInfo: UserInfo, qualifiedSegments?: string[]): Promise; onUserUpdate(handler: OnUserUpdateHandler): DisposeFn; isReady(): boolean; getIsReadyPromiseFulfilled(): boolean; @@ -381,7 +381,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return await this.userContext.fetchQualifiedSegments(options); } - public async setUser(userInfo: UserInfo): Promise { + public async setUser(userInfo: UserInfo, qualifiedSegments?: string[]): Promise { // If user id is not present and ODP is explicitly off, user promise will be pending until setUser is called again with proper user id if (userInfo.id === null && this.odpExplicitlyOff) { return; @@ -404,6 +404,9 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // synchronous user context setting is required including for server side rendering (SSR) this.setCurrentUserContext(userInfo); + if (this.userContext && !this.odpExplicitlyOff && this._client?.isOdpIntegrated() && qualifiedSegments) { + this.userContext.qualifiedSegments = qualifiedSegments; + } // we need to wait for fetch qualified segments success for failure await this._client?.onReady(); } diff --git a/src/hooks.spec.tsx b/src/hooks.spec.tsx index 865dd01..c792054 100644 --- a/src/hooks.spec.tsx +++ b/src/hooks.spec.tsx @@ -1061,6 +1061,25 @@ describe('hooks', () => { await waitFor(() => expect(mockLog).toHaveBeenCalledWith(true)); }); + it('should pass qualifiedSegments to setUser when provided via OptimizelyProvider', async () => { + const segments = ['segment1', 'segment2']; + decideMock.mockReturnValue({ ...defaultDecision, enabled: true }); + + render( + + + + ); + + await waitFor(() => { + expect(optimizelyMock.setUser).toHaveBeenCalledWith({ id: 'testuser', attributes: {} }, segments); + }); + }); + it('should re-render after updating the override user ID argument', async () => { decideMock.mockReturnValue({ ...defaultDecision }); const { rerender } = render( diff --git a/src/withOptimizely.spec.tsx b/src/withOptimizely.spec.tsx index f68626e..afc0e6c 100644 --- a/src/withOptimizely.spec.tsx +++ b/src/withOptimizely.spec.tsx @@ -70,7 +70,7 @@ describe('withOptimizely', () => { ); await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); - expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes }); + expect(optimizelyClient.setUser).toHaveBeenCalledWith({ id: userId, attributes }, undefined); }); }); @@ -84,10 +84,13 @@ describe('withOptimizely', () => { ); await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); - expect(optimizelyClient.setUser).toHaveBeenCalledWith({ - id: userId, - attributes: {}, - }); + expect(optimizelyClient.setUser).toHaveBeenCalledWith( + { + id: userId, + attributes: {}, + }, + undefined + ); }); }); @@ -101,10 +104,13 @@ describe('withOptimizely', () => { ); await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); - expect(optimizelyClient.setUser).toHaveBeenCalledWith({ - id: userId, - attributes: {}, - }); + expect(optimizelyClient.setUser).toHaveBeenCalledWith( + { + id: userId, + attributes: {}, + }, + undefined + ); }); }); @@ -119,10 +125,13 @@ describe('withOptimizely', () => { ); await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); - expect(optimizelyClient.setUser).toHaveBeenCalledWith({ - id: userId, - attributes, - }); + expect(optimizelyClient.setUser).toHaveBeenCalledWith( + { + id: userId, + attributes, + }, + undefined + ); }); }); @@ -143,10 +152,13 @@ describe('withOptimizely', () => { ); await waitFor(() => expect(optimizelyClient.setUser).toHaveBeenCalledTimes(1)); - expect(optimizelyClient.setUser).toHaveBeenCalledWith({ - id: userId, - attributes, - }); + expect(optimizelyClient.setUser).toHaveBeenCalledWith( + { + id: userId, + attributes, + }, + undefined + ); }); }); From 2d108630faf9b0a7d71e2550438c7c98d4ece516 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:45:51 +0600 Subject: [PATCH 13/13] [FSSDK-10777] doc update --- README.md | 59 +++++++++++++++++++++----------------- docs/nextjs-integration.md | 42 ++++++++++++++++++--------- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index f3b90fc..83c7c6b 100644 --- a/README.md +++ b/README.md @@ -102,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[]` | No | Pre-fetched ODP audience segments for the user. Useful during SSR where async segment fetching is unavailable. | ### Readiness @@ -382,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 @@ -445,6 +450,8 @@ function MyComponent() { | `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`. See the [Limitations — ODP segments](#limitations) section for details. + ### React Server Components 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: @@ -479,7 +486,7 @@ For detailed Next.js examples covering both App Router and Pages Router patterns - **Datafile required** — SSR requires a pre-fetched datafile. Using `sdkKey` alone falls back to a failed decision. - **Static user only** — User `Promise` is not supported during SSR. -- **ODP segments unavailable** — ODP audience segments require async I/O and are not available during server rendering. +- **ODP segments** — ODP audience segments require async I/O and are not available during server rendering. Pass pre-fetched segments 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). diff --git a/docs/nextjs-integration.md b/docs/nextjs-integration.md index 5a7b403..0430b2e 100644 --- a/docs/nextjs-integration.md +++ b/docs/nextjs-integration.md @@ -16,7 +16,7 @@ You will need your Optimizely SDK key, available from the Optimizely app under * 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 many ways to pre-fetch the datafile on the server. Below are two common approaches you could follow. +There are several ways to pre-fetch the datafile on the server. Below are two common approaches you could follow. ## Next.js App Router @@ -24,11 +24,9 @@ In the App Router, fetch the datafile in an async server component (e.g., your r ### 1. Create a datafile fetcher -There are several ways to fetch the datafile. Here are two common approaches: - **Option A: Using the SDK's built-in datafile fetching (Recommended)** -Create an SDK instance with your `sdkKey` and let it fetch and cache the datafile internally. 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 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 @@ -38,18 +36,23 @@ const pollingInstance = createInstance({ sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || "", }); -export async function getDatafile() { - pollingInstance.setUser({ - id: "dummyUser", - }); - await pollingInstance.onReady(); - return pollingInstance.getOptimizelyConfig()?.getDatafile(); +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 Optimizely's CDN. This is simpler and gives you full control over caching (e.g., via Next.js `fetch` options like `next.revalidate`), but does not include automatic polling for updates. +Fetch the datafile directly from CDN. ```ts // src/data/getDatafile.ts @@ -270,6 +273,19 @@ User `Promise` is not supported during SSR. You must provide a static user objec ``` -### ODP audience segments unavailable +### 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} + +``` -ODP (Optimizely Data Platform) audience segments require fetching segment data via an async network call, which is not available during server rendering. Decisions will be made without audience segment data. If your experiment relies on ODP segments, consider using the loading state pattern above and deferring the decision to the client. +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.