Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ lib
.npmrc
dist/
build/
.build/
.github/prompts/
.rpt2_cache
.env

Expand Down
222 changes: 128 additions & 94 deletions README.md

Large diffs are not rendered by default.

331 changes: 331 additions & 0 deletions docs/nextjs-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
# Next.js Integration Guide

This guide covers how to use the Optimizely React SDK with Next.js for server-side rendering (SSR), static site generation (SSG), and React Server Components.

## Prerequisites

Install the React SDK:

```bash
npm install @optimizely/react-sdk
```

You will need your Optimizely SDK key, available from the Optimizely app under **Settings > Environments**.

## SSR with Pre-fetched Datafile

Server-side rendering requires a pre-fetched datafile. The SDK cannot fetch the datafile asynchronously during server rendering, so you must fetch it beforehand and pass it to `createInstance`.

There are several ways to pre-fetch the datafile on the server. Below are two common approaches you could follow.

## Next.js App Router

In the App Router, fetch the datafile in an async server component (e.g., your root layout) and pass it as a prop to a client-side provider.

### 1. Create a datafile fetcher

**Option A: Using the SDK's built-in datafile fetching (Recommended)**

Create a module-level SDK instance with your `sdkKey` and use a notification listener to detect when the datafile is ready. This approach benefits from the SDK's built-in polling and caching, making it suitable when you want automatic datafile updates across requests.

```ts
// src/data/getDatafile.ts
import { createInstance } from '@optimizely/react-sdk';

const pollingInstance = createInstance({
sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || "",
});

const pollingInstance = createInstane();

const configReady = new Promise<void>((resolve) => {
pollingInstance.notificationCenter.addNotificationListener(
enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE,
() => resolve();
);
}

export function getDatafile(): Promise<string | undefined> {
return configReady.then(() => pollingInstance.getOptimizelyConfig()?.getDatafile());
}
```

**Option B: Direct CDN fetch**

Fetch the datafile directly from CDN.

```ts
// src/data/getDatafile.ts
const CDN_URL = `https://cdn.optimizely.com/datafiles/${process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY}.json`;

export async function getDatafile() {
const res = await fetch(CDN_URL);

if (!res.ok) {
throw new Error(`Failed to fetch datafile: ${res.status}`);
}

return res.json();
}
```

### 2. Create a client-side provider

Since `OptimizelyProvider` uses React Context (a client-side feature), it must be wrapped in a `'use client'` component:

```tsx
// src/providers/OptimizelyProvider.tsx
'use client';

import { OptimizelyProvider, createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk';
import { ReactNode, useState } from 'react';

export function OptimizelyClientProvider({ children, datafile }: { children: ReactNode; datafile: object }) {
const isServerSide = typeof window === 'undefined';

const [optimizely] = useState(() =>
createInstance({
datafile,
sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '',
datafileOptions: { autoUpdate: !isServerSide },
defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [],
odpOptions: {
disabled: isServerSide,
},
})
);

return (
<OptimizelyProvider optimizely={optimizely} user={{ id: 'user123', attributes: { plan_type: 'premium' } }} isServerSide={isServerSide}>
{children}
</OptimizelyProvider>
);
}
```

> See [Configuring the instance for server use](../README.md#configuring-the-instance-for-server-use) in the README for an explanation of each option.

### 3. Wire it up in your root layout

```tsx
// src/app/layout.tsx
import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider';
import { getDatafile } from '@/data/getDatafile';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
const datafile = await getDatafile();

return (
<html lang="en">
<body>
<OptimizelyClientProvider datafile={datafile}>{children}</OptimizelyClientProvider>
</body>
</html>
);
}
```

#### Pre-fetching ODP audience segments

If your project uses ODP audience segments, you can pre-fetch them server-side using `getQualifiedSegments` and pass them to the provider via the `qualifiedSegments` prop.

```tsx
// src/app/layout.tsx
import { getQualifiedSegments } from '@optimizely/react-sdk';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
const datafile = await getDatafile();
const segments = await getQualifiedSegments('user-123', datafile);

return (
<html lang="en">
<body>
<OptimizelyClientProvider datafile={datafile} qualifiedSegments={segments}>
{children}
</OptimizelyClientProvider>
</body>
</html>
);
}
```

> **Caching recommendation:** The ODP segment fetch adds latency to initial page loads. Consider caching the result per user to avoid re-fetching on every request.

## Next.js Pages Router

In the Pages Router, fetch the datafile server-side and pass it as a prop. There are three data-fetching strategies depending on your needs.

### 1. Create a client-side provider

Same as the [App Router provider](#2-create-a-client-side-provider) above (without the `'use client'` directive, which is not needed in Pages Router).

### 2. Fetch the datafile

Choose the data-fetching strategy that best fits your use case:

#### Option A: `getInitialProps` — app-wide setup

Fetches the datafile for every page via `_app.tsx`. Useful when you want Optimizely available globally across all pages.

```tsx
// pages/_app.tsx
import { OptimizelyClientProvider } from '@/providers/OptimizelyProvider';
import type { AppProps, AppContext } from 'next/app';
import { getDatafile } from '@/data/getDatafile';

export default function App({ Component, pageProps }: AppProps) {
return (
<OptimizelyClientProvider datafile={pageProps.datafile}>
<Component {...pageProps} />
</OptimizelyClientProvider>
);
}

App.getInitialProps = async (appContext: AppContext) => {
const appProps = await App.getInitialProps(appContext);
const datafile = await getDatafile();
return { ...appProps, pageProps: { ...appProps.pageProps, datafile } };
};
```

Similar to App Router example, if you have ODP enabled and want to pre-fetch segments, you can do following -

```tsx
import { getQualifiedSegments } from "@optimizely/react-sdk";

App.getInitialProps = async (appContext: AppContext) => {
const appProps = await App.getInitialProps(appContext);
const datafile = await getDatafile();
const segments = await getQualifiedSegments('user-123', datafile);
return { ...appProps, pageProps: { ...appProps.pageProps, datafile, segments } };
};
```


#### Option B: `getServerSideProps` — per-page setup

Fetches the datafile per request on specific pages. Useful when only certain pages need feature flags.

```tsx
// pages/index.tsx
export async function getServerSideProps() {
const datafile = await getDatafile();

return { props: { datafile } };
}
```

#### Option C: `getStaticProps` — static generation with revalidation

Fetches the datafile at build time and revalidates periodically. Best for static pages where per-request freshness is not critical.

```tsx
// pages/index.tsx
export async function getStaticProps() {
const datafile = await getDatafile();

return {
props: { datafile },
revalidate: 60, // re-fetch every 60 seconds
};
}
```

## Using Feature Flags in Client Components

Once the provider is set up, use the `useDecision` hook in any client component:

```tsx
'use client';

import { useDecision } from '@optimizely/react-sdk';

export default function FeatureBanner() {
const [decision] = useDecision('banner-flag');

return decision.enabled ? <h1>New Banner</h1> : <h1>Default Banner</h1>;
}
```

## 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 (
<OptimizelyProvider optimizely={optimizely} user={{ id: 'user123' }}>
<FeatureBanner />
</OptimizelyProvider>
);
}

function FeatureBanner() {
const [decision, isClientReady, didTimeout] = useDecision('banner-flag');

if (!isClientReady && !didTimeout) {
return <h1>Loading...</h1>;
}

return decision.enabled ? <h1>New Banner</h1> : <h1>Default Banner</h1>;
}
```

## 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 <h1>Loading...</h1>;
}

return decision.enabled ? <h1>Feature Enabled</h1> : <h1>Feature Disabled</h1>;
}
```

### User Promise not supported

User `Promise` is not supported during SSR. You must provide a static user object to `OptimizelyProvider`:

```tsx
// Supported
<OptimizelyProvider user={{ id: 'user123', attributes: { plan: 'premium' } }} ... />

// NOT supported during SSR
<OptimizelyProvider user={fetchUserPromise} ... />
```

### 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
<OptimizelyProvider
optimizely={optimizely}
user={{ id: 'user123' }}
qualifiedSegments={['segment1', 'segment2']}
isServerSide={isServerSide}
>
{children}
</OptimizelyProvider>
```

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.
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
13 changes: 12 additions & 1 deletion scripts/build.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand Down Expand Up @@ -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',
});
Loading