Next.js integration for the React SDK
How to use the Optimizely Feature Experimentation React SDK with Next.js for server-side rendering 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
- Install the React SDK.
- 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 your sdkKey and use a notification listener to detect when the datafile is ready. This approach benefits from the React SDK's built-in polling and caching, making it suitable when you want automatic datafile updates across requests.
// 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.
// 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.
// 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 Configure the instance for server use for an explanation of each option.
Wire it up in your root layout
// 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-fetch ODP audience segments
If your project uses (Optimizely data platform) ODP audience segments, you can pre-fetch them server-side using getQualifiedSegments and pass them to the provider via the qualifiedSegments prop.
// 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>
);
}
NoteThe 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 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
getInitialProps – app-wide configurationFetches the datafile for every page with _app.tsx. This is useful when you want Optimizely available globally across all pages.
// 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:
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
getServerSideProps – per-page configurationFetches the datafile per request on specific pages. Useful when only certain pages need feature flags.
// pages/index.tsx
export async function getServerSideProps() {
const datafile = await getDatafile();
return { props: { datafile } };
}Option C: getStaticProps – static generation with revalidation
getStaticProps – static generation with revalidationFetches the datafile at build time and revalidates periodically. Best for static pages where per-request freshness is not critical.
// 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 useDecision hook in any client component.
'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 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.
'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 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.
'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
Promise not supportedUser Promise is not supported during SSR. You must provide a static user object to OptimizelyProvider.
// Supported
<OptimizelyProvider user={{ id: 'user123', attributes: { plan: 'premium' } }} ... />
// NOT supported during SSR
<OptimizelyProvider 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.
<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 are made without audience segment data. In that case, consider deferring the decision to the client using the loading state fallback pattern described previously, where ODP segments are fetched automatically when ODP is enabled.
Source files
The language and platform source files containing the implementation for React SDK are available on GitHub.
Updated 17 minutes ago
