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 1/9] [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 2/9] [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 3/9] [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 4/9] [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 5/9] [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 6/9] [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 7/9] [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 8/9] [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 9/9] [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