-
Notifications
You must be signed in to change notification settings - Fork 36
[FSSDK-10777] SSR improvements + RSC support addition #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
6165ef7
[FSSDK-12249] gitignore update
junaed-optimizely 6a836f5
[FSSDK-10777] ssr support update
junaed-optimizely 7c2cb07
[FSSDK-10777] ssr support update
junaed-optimizely 68923cf
[FSSDK-10777] doc improvement
junaed-optimizely 2af637d
[FSSDK-10777] doc improvement
junaed-optimizely 40c71cc
[FSSDK-10777] doc improvement
junaed-optimizely be511d3
[FSSDK-10777] improvement
junaed-optimizely 252515f
[FSSDK-10777] doc improvement
junaed-optimizely 05a4a2e
[FSSDK-10777] doc improvement
junaed-optimizely 892fca6
[FSSDK-10777] ssg doc update
junaed-optimizely c28e1c8
[FSSDK-10777] server bundle addition
junaed-optimizely 441bf83
[FSSDK-10777] segments support addition
junaed-optimizely 2d10863
[FSSDK-10777] doc update
junaed-optimizely 2bedde4
[FSSDK-10773] hook logic improvement
junaed-optimizely 332f098
[FSSDK-10773] getQualifiedSegments addition
junaed-optimizely f284c99
[FSSDK-10773] doc update
junaed-optimizely 6bca626
[FSSDK-10777] update
junaed-optimizely File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ lib | |
| .npmrc | ||
| dist/ | ||
| build/ | ||
| .build/ | ||
| .github/prompts/ | ||
| .rpt2_cache | ||
| .env | ||
|
|
||
|
|
||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
junaed-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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() { | ||
junaed-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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`: | ||
junaed-optimizely marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.