---
title: Next.js integration for the React SDK v4+
slug: feature-experimentation/docs/nextjs-integration-for-the-react-sdk
docTags: 
createdAt: 2026-08-05T20:39:52.000Z
---

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](docId:4Ocp8_EYU66GvsPjqyZzz).
- 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 React 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. The following are two common approaches you could follow:

## Next.js App Router

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

### Create a datafile fetcher

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

Create a module-level SDK instance with a polling config manager 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.

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

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

const configReady = new Promise<void>((resolve) => {
  pollingInstance.notificationCenter.addNotificationListener(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.

```typescript
// 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();
}
```

### Create a client-side provider

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

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

import {
  OptimizelyProvider,
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
  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({
      projectConfigManager: createPollingProjectConfigManager({
        sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '',
        datafile,
      }),
      eventProcessor: createBatchEventProcessor(),
      defaultDecideOptions: isServerSide ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [],
      disposable: isServerSide,
    })
  );

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

### Module-level alternative

You can also create the client at module level to avoid recreating the instance on re-renders. The trade-off is that the datafile must be resolved before the provider module is evaluated. One approach is to use `globalThis` to bridge the datafile between server and client: the server fetches the datafile, sets it on `globalThis` for server rendering, and injects a `<script>` tag so the client has it at module evaluation time.

Both examples below reuse the `getDatafile` helper from [Option A](docId\:Vn596IAXbScH8NzaU5RoV) or [Option B](docId\:Vn596IAXbScH8NzaU5RoV) above.

**App Router (**`layout.tsx`**)**

The layout fetches the datafile, sets `globalThis` for the server render, and injects a `<script>` tag for the client.

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

export default async function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
  const datafile = await getDatafile();
  const serialized = JSON.stringify(datafile ?? '');
  globalThis.__OPTIMIZELY_DATAFILE__ = datafile ?? '';

  return (
    <html lang="en">
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `globalThis.__OPTIMIZELY_DATAFILE__ = ${serialized};`,
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}
```

**Pages Router (**`_document.tsx`**)**

In the Pages Router, use `_document.tsx` for the same purpose — fetch the datafile in `getInitialProps`, set `globalThis` for the server render, and inject the `<script>` tag.

```javascript
// pages/_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { getDatafile } from '@/data/getDatafile';

export default function MyDocument({ datafile }: { datafile: string }) {
  const serialized = JSON.stringify(datafile ?? '');
  return (
    <Html>
      <Head>
        <script
          dangerouslySetInnerHTML={{
            __html: `globalThis.__OPTIMIZELY_DATAFILE__ = ${serialized};`,
          }}
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

MyDocument.getInitialProps = async (ctx) => {
  const initialProps = await Document.getInitialProps(ctx);
  const datafile = await getDatafile();
  globalThis.__OPTIMIZELY_DATAFILE__ = datafile;
  return { ...initialProps, datafile };
};
```

**Provider (module-level)**

With `globalThis.__OPTIMIZELY_DATAFILE__` available, the provider can create the client at module level with immediate datafile readiness:

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

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

const optimizely = createInstance({
  projectConfigManager: createPollingProjectConfigManager({
    sdkKey: process.env.NEXT_PUBLIC_OPTIMIZELY_SDK_KEY || '',
    datafile: globalThis.__OPTIMIZELY_DATAFILE__,
  }),
  eventProcessor: createBatchEventProcessor(),
});

export function OptimizelyClientProvider({ children }: { children: ReactNode }) {
  return (
    <OptimizelyProvider client={optimizely} user={{ id: 'user123', attributes: { plan_type: 'premium' } }}>
      {children}
    </OptimizelyProvider>
  );
}
```

:::BlockQuote
See [Server-Side Rendering](docId\:jhRg84MqBcOq1Il6NaBfB) in the initialization guide for an explanation of each option.
:::

### Wire it up in your root layout

:::BlockQuote
If you are using the [module-level alternative](docId\:Vn596IAXbScH8NzaU5RoV), the layout already handles datafile injection — just wrap `{children}` with `<OptimizelyClientProvider>` (no `datafile` prop needed).
:::

```javascript
// 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 [(Optimizely data platform) ODP audience segments](docId:6UBBxxRv331nBIVZ2lun8), you can pre-fetch them server-side using `getQualifiedSegments` and pass them to the provider via the `qualifiedSegments` prop.

```javascript
// 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>
  );
}
```

:::hint{type="info"}
**Note**
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.

### Create a client-side provider

Same as the [App Router provider](docId\:Vn596IAXbScH8NzaU5RoV) previously (without the `'use client'` directive, which is not needed in Pages Router).

### Fetch the datafile

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

### Option A: `getInitialProps` – app-wide configuration

Fetches the datafile for every page with `_app.tsx`. This is useful when you want Optimizely available globally across all pages.

```javascript
// 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 the App Router example, if you have ODP enabled and want to pre-fetch segments, you can do the following:

```javascript
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 configuration

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

```javascript
// 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.

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

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

## Use feature flags in client components

After you configure the provider, use the `useDecide` hook in any client component:

```javascript
'use client';

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

export default function FeatureBanner() {
  const { decision, isLoading } = useDecide('banner-flag');

  if (isLoading) return <h1>Loading...</h1>;

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

## Static site generation (SSG)

For statically generated pages, the React 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.

```javascript
'use client';
import { useState } from 'react';
import {
  OptimizelyProvider,
  createInstance,
  createPollingProjectConfigManager,
  createBatchEventProcessor,
  useDecide,
} from '@optimizely/react-sdk';

export function App() {
  const [client] = useState(() =>
    createInstance({
      projectConfigManager: createPollingProjectConfigManager({ sdkKey: 'YOUR_SDK_KEY' }),
      eventProcessor: createBatchEventProcessor(),
    })
  );

  return (
    <OptimizelyProvider client={client} user={{ id: 'user123' }}>
      <FeatureBanner />
    </OptimizelyProvider>
  );
}

function FeatureBanner() {
  const { decision, isLoading } = useDecide('banner-flag');

  if (isLoading) return <h1>Loading...</h1>;

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

## Limitations

### Datafile required for SSR

SSR with the `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 fall back to defaults.

To handle this gracefully, render a loading state and let the client hydrate with the real decision.

```javascript
'use client';

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

export default function MyFeature() {
  const { decision, isLoading } = useDecide('flag-1');

  if (isLoading) return <h1>Loading...</h1>;

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

### User `Promise` not supported

User `Promise` is not supported. You can pass `null`, `undefined`, or omit the `user` prop while user information is being fetched — hooks will return `{ isLoading: true }` until a resolved user object is provided. For VUID-only mode (no user ID), pass `user={{}}`.

```javascript
// Supported
<OptimizelyProvider client={optimizely} user={{ id: 'user123', attributes: { plan: 'premium' } }} />

// Supported — hooks return { isLoading: true } until user is provided
<OptimizelyProvider client={optimizely} user={null} />

// NOT supported
<OptimizelyProvider client={optimizely} user={fetchUserPromise} />
```

### ODP audience segments

ODP audience segments require fetching segment data through an async network call, which is not available during server rendering. To include segment data during SSR, pass pre-fetched segments with the `qualifiedSegments` prop on `OptimizelyProvider`.

```javascript
<OptimizelyProvider
  client={optimizely}
  user={{ id: 'user123' }}
  qualifiedSegments={['segment1', 'segment2']}
  skipSegments={true}
>
  {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 via `createOdpManager`.

## Source files

The language and platform source files containing the implementation for the React SDK are available on [GitHub](https://github.com/optimizely/react-sdk/blob/master/src/index.ts).
