Skip to content

Commit c23e1ef

Browse files
[FSSDK-10777] SSR improvements + RSC support addition (#318)
1 parent a6a7f5c commit c23e1ef

22 files changed

+1211
-281
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ lib
77
.npmrc
88
dist/
99
build/
10+
.build/
11+
.github/prompts/
1012
.rpt2_cache
1113
.env
1214

README.md

Lines changed: 128 additions & 94 deletions
Large diffs are not rendered by default.

docs/nextjs-integration.md

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
# Next.js Integration Guide
2+
3+
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.
4+
5+
## Prerequisites
6+
7+
Install the React SDK:
8+
9+
```bash
10+
npm install @optimizely/react-sdk
11+
```
12+
13+
You will need your Optimizely SDK key, available from the Optimizely app under **Settings > Environments**.
14+
15+
## SSR with Pre-fetched Datafile
16+
17+
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`.
18+
19+
There are several ways to pre-fetch the datafile on the server. Below are two common approaches you could follow.
20+
21+
## Next.js App Router
22+
23+
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.
24+
25+
### 1. Create a datafile fetcher
26+
27+
**Option A: Using the SDK's built-in datafile fetching (Recommended)**
28+
29+
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.
30+
31+
```ts
32+
// src/data/getDatafile.ts
33+
import { createInstance } from '@optimizely/react-sdk';
34+
35+
const pollingInstance = createInstance({
36+
sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || "",
37+
});
38+
39+
const pollingInstance = createInstane();
40+
41+
const configReady = new Promise<void>((resolve) => {
42+
pollingInstance.notificationCenter.addNotificationListener(
43+
enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE,
44+
() => resolve();
45+
);
46+
}
47+
48+
export function getDatafile(): Promise<string | undefined> {
49+
return configReady.then(() => pollingInstance.getOptimizelyConfig()?.getDatafile());
50+
}
51+
```
52+
53+
**Option B: Direct CDN fetch**
54+
55+
Fetch the datafile directly from CDN.
56+
57+
```ts
58+
// src/data/getDatafile.ts
59+
const CDN_URL = `https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`;
60+
61+
export async function getDatafile() {
62+
const res = await fetch(CDN_URL);
63+
64+
if (!res.ok) {
65+
throw new Error(`Failed to fetch datafile: ${res.status}`);
66+
}
67+
68+
return res.json();
69+
}
70+
```
71+
72+
### 2. Create a client-side provider
73+
74+
Since `OptimizelyProvider` uses React Context (a client-side feature), it must be wrapped in a `'use client'` component:
75+
76+
```tsx
77+
// src/providers/OptimizelyProvider.tsx
78+
'use client';
79+
80+
import { OptimizelyProvider, createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk';
81+
import { ReactNode, useState } from 'react';
82+
83+
export function OptimizelyClientProvider({ children, datafile }: { children: ReactNode; datafile: object }) {
84+
const isServerSide = typeof window === 'undefined';
85+
86+
const [optimizely] = useState(() =>
87+
createInstance({
88+
datafile,
89+
sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '',
90+
datafileOptions: { autoUpdate: !isServerSide },
91+
defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [],
92+
odpOptions: {
93+
disabled: isServerSide,
94+
},
95+
})
96+
);
97+
98+
return (
99+
<OptimizelyProvider optimizely={optimizely} user={{ id: 'user123', attributes: { plan_type: 'premium' } }} isServerSide={isServerSide}>
100+
{children}
101+
</OptimizelyProvider>
102+
);
103+
}
104+
```
105+
106+
> See [Configuring the instance for server use](../README.md#configuring-the-instance-for-server-use) in the README for an explanation of each option.
107+
108+
### 3. Wire it up in your root layout
109+
110+
```tsx
111+
// src/app/layout.tsx
112+
import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider';
113+
import { getDatafile } from '@/data/getDatafile';
114+
115+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
116+
const datafile = await getDatafile();
117+
118+
return (
119+
<html lang="en">
120+
<body>
121+
<OptimizelyClientProvider datafile={datafile}>{children}</OptimizelyClientProvider>
122+
</body>
123+
</html>
124+
);
125+
}
126+
```
127+
128+
#### Pre-fetching ODP audience segments
129+
130+
If your project uses ODP audience segments, you can pre-fetch them server-side using `getQualifiedSegments` and pass them to the provider via the `qualifiedSegments` prop.
131+
132+
```tsx
133+
// src/app/layout.tsx
134+
import { getQualifiedSegments } from '@optimizely/react-sdk';
135+
136+
export default async function RootLayout({ children }: { children: React.ReactNode }) {
137+
const datafile = await getDatafile();
138+
const segments = await getQualifiedSegments('user-123', datafile);
139+
140+
return (
141+
<html lang="en">
142+
<body>
143+
<OptimizelyClientProvider datafile={datafile} qualifiedSegments={segments}>
144+
{children}
145+
</OptimizelyClientProvider>
146+
</body>
147+
</html>
148+
);
149+
}
150+
```
151+
152+
> **Caching recommendation:** The ODP segment fetch adds latency to initial page loads. Consider caching the result per user to avoid re-fetching on every request.
153+
154+
## Next.js Pages Router
155+
156+
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.
157+
158+
### 1. Create a client-side provider
159+
160+
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).
161+
162+
### 2. Fetch the datafile
163+
164+
Choose the data-fetching strategy that best fits your use case:
165+
166+
#### Option A: `getInitialProps` — app-wide setup
167+
168+
Fetches the datafile for every page via `_app.tsx`. Useful when you want Optimizely available globally across all pages.
169+
170+
```tsx
171+
// pages/_app.tsx
172+
import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider';
173+
import type { AppProps, AppContext } from 'next/app';
174+
import { getDatafile } from '@/data/getDatafile';
175+
176+
export default function App({ Component, pageProps }: AppProps) {
177+
return (
178+
<OptimizelyClientProvider datafile={pageProps.datafile}>
179+
<Component {...pageProps} />
180+
</OptimizelyClientProvider>
181+
);
182+
}
183+
184+
App.getInitialProps = async (appContext: AppContext) => {
185+
const appProps = await App.getInitialProps(appContext);
186+
const datafile = await getDatafile();
187+
return { ...appProps, pageProps: { ...appProps.pageProps, datafile } };
188+
};
189+
```
190+
191+
Similar to App Router example, if you have ODP enabled and want to pre-fetch segments, you can do following -
192+
193+
```tsx
194+
import { getQualifiedSegments } from "@optimizely/react-sdk";
195+
196+
App.getInitialProps = async (appContext: AppContext) => {
197+
const appProps = await App.getInitialProps(appContext);
198+
const datafile = await getDatafile();
199+
const segments = await getQualifiedSegments('user-123', datafile);
200+
return { ...appProps, pageProps: { ...appProps.pageProps, datafile, segments } };
201+
};
202+
```
203+
204+
205+
#### Option B: `getServerSideProps` — per-page setup
206+
207+
Fetches the datafile per request on specific pages. Useful when only certain pages need feature flags.
208+
209+
```tsx
210+
// pages/index.tsx
211+
export async function getServerSideProps() {
212+
const datafile = await getDatafile();
213+
214+
return { props: { datafile } };
215+
}
216+
```
217+
218+
#### Option C: `getStaticProps` — static generation with revalidation
219+
220+
Fetches the datafile at build time and revalidates periodically. Best for static pages where per-request freshness is not critical.
221+
222+
```tsx
223+
// pages/index.tsx
224+
export async function getStaticProps() {
225+
const datafile = await getDatafile();
226+
227+
return {
228+
props: { datafile },
229+
revalidate: 60, // re-fetch every 60 seconds
230+
};
231+
}
232+
```
233+
234+
## Using Feature Flags in Client Components
235+
236+
Once the provider is set up, use the `useDecision` hook in any client component:
237+
238+
```tsx
239+
'use client';
240+
241+
import { useDecision } from '@optimizely/react-sdk';
242+
243+
export default function FeatureBanner() {
244+
const [decision] = useDecision('banner-flag');
245+
246+
return decision.enabled ? <h1>New Banner</h1> : <h1>Default Banner</h1>;
247+
}
248+
```
249+
250+
## Static Site Generation (SSG)
251+
252+
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.
253+
254+
```tsx
255+
'use client';
256+
257+
import { OptimizelyProvider, createInstance, useDecision } from '@optimizely/react-sdk';
258+
259+
const optimizely = createInstance({ sdkKey: 'YOUR_SDK_KEY' });
260+
261+
export function App() {
262+
return (
263+
<OptimizelyProvider optimizely={optimizely} user={{ id: 'user123' }}>
264+
<FeatureBanner />
265+
</OptimizelyProvider>
266+
);
267+
}
268+
269+
function FeatureBanner() {
270+
const [decision, isClientReady, didTimeout] = useDecision('banner-flag');
271+
272+
if (!isClientReady && !didTimeout) {
273+
return <h1>Loading...</h1>;
274+
}
275+
276+
return decision.enabled ? <h1>New Banner</h1> : <h1>Default Banner</h1>;
277+
}
278+
```
279+
280+
## Limitations
281+
282+
### Datafile required for SSR
283+
284+
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.
285+
286+
To handle this gracefully, render a loading state and let the client hydrate with the real decision:
287+
288+
```tsx
289+
'use client';
290+
291+
import { useDecision } from '@optimizely/react-sdk';
292+
293+
export default function MyFeature() {
294+
const [decision, isClientReady, didTimeout] = useDecision('flag-1');
295+
296+
if (!didTimeout && !isClientReady) {
297+
return <h1>Loading...</h1>;
298+
}
299+
300+
return decision.enabled ? <h1>Feature Enabled</h1> : <h1>Feature Disabled</h1>;
301+
}
302+
```
303+
304+
### User Promise not supported
305+
306+
User `Promise` is not supported during SSR. You must provide a static user object to `OptimizelyProvider`:
307+
308+
```tsx
309+
// Supported
310+
<OptimizelyProvider user={{ id: 'user123', attributes: { plan: 'premium' } }} ... />
311+
312+
// NOT supported during SSR
313+
<OptimizelyProvider user={fetchUserPromise} ... />
314+
```
315+
316+
### ODP audience segments
317+
318+
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`:
319+
320+
```tsx
321+
<OptimizelyProvider
322+
optimizely={optimizely}
323+
user={{ id: 'user123' }}
324+
qualifiedSegments={['segment1', 'segment2']}
325+
isServerSide={isServerSide}
326+
>
327+
{children}
328+
</OptimizelyProvider>
329+
```
330+
331+
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.

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99
"types": "dist/index.d.ts",
1010
"main": "dist/react-sdk.js",
1111
"browser": "dist/react-sdk.js",
12+
"exports": {
13+
".": {
14+
"react-server": {
15+
"types": "./dist/server.d.ts",
16+
"import": "./dist/server.es.js",
17+
"require": "./dist/server.js",
18+
"default": "./dist/server.js"
19+
},
20+
"types": "./dist/index.d.ts",
21+
"import": "./dist/react-sdk.es.js",
22+
"require": "./dist/react-sdk.js",
23+
"default": "./dist/react-sdk.js"
24+
}
25+
},
1226
"directories": {
1327
"lib": "lib"
1428
},

scripts/build.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2019, 2023 Optimizely
2+
* Copyright 2019, 2023, 2026 Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
/* eslint-disable @typescript-eslint/no-var-requires */
1718
const path = require('path');
1819
const execSync = require('child_process').execSync;
1920

@@ -46,3 +47,13 @@ exec(`./node_modules/.bin/rollup -c scripts/config.js -f system -o dist/${packag
4647
EXTERNALS: 'forBrowsers',
4748
BUILD_ENV: 'production',
4849
});
50+
51+
console.log('\nBuilding server ES modules...');
52+
exec(`./node_modules/.bin/rollup -c scripts/config.js -f es -o dist/server.es.js`, {
53+
ENTRY: 'src/server.ts',
54+
});
55+
56+
console.log('\nBuilding server CommonJS modules...');
57+
exec(`./node_modules/.bin/rollup -c scripts/config.js -f cjs -o dist/server.js`, {
58+
ENTRY: 'src/server.ts',
59+
});

0 commit comments

Comments
 (0)