-
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
Open
junaed-optimizely
wants to merge
10
commits into
3.x.x
Choose a base branch
from
junaed/fssdk-10777-ssr-doc-update
base: 3.x.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 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 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 | ||
|
|
||
|
|
||
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 |
|---|---|---|
|
|
@@ -12,7 +12,6 @@ Refer to the [React SDK's developer documentation](https://docs.developers.optim | |
|
|
||
| For React Native, review the [React Native developer documentation](https://docs.developers.optimizely.com/feature-experimentation/docs/javascript-react-native-sdk). | ||
|
|
||
|
|
||
| ### Features | ||
|
|
||
| - Automatic datafile downloading | ||
|
|
@@ -28,11 +27,7 @@ The React SDK is compatible with `React 16.8.0 +` | |
| ### Example | ||
|
|
||
| ```jsx | ||
| import { | ||
| createInstance, | ||
| OptimizelyProvider, | ||
| useDecision, | ||
| } from '@optimizely/react-sdk'; | ||
| import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/react-sdk'; | ||
|
|
||
| const optimizelyClient = createInstance({ | ||
| sdkKey: 'your-optimizely-sdk-key', | ||
|
|
@@ -43,8 +38,8 @@ function MyComponent() { | |
| return ( | ||
| <React.Fragment> | ||
| <SearchComponent algorithm={decision.variables.algorithm} /> | ||
| { decision.variationKey === 'relevant_first' && <RelevantFirstList /> } | ||
| { decision.variationKey === 'recent_first' && <RecentFirstList /> } | ||
| {decision.variationKey === 'relevant_first' && <RelevantFirstList />} | ||
| {decision.variationKey === 'recent_first' && <RecentFirstList />} | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
|
|
@@ -70,7 +65,8 @@ class App extends React.Component { | |
| npm install @optimizely/react-sdk | ||
| ``` | ||
|
|
||
| For **React Native**, installation instruction is bit different. Check out the | ||
| For **React Native**, installation instruction is bit different. Check out the | ||
|
|
||
| - [Official Installation guide](https://docs.developers.optimizely.com/feature-experimentation/docs/install-sdk-reactnative) | ||
| - [Expo React Native Sample App](https://github.com/optimizely/expo-react-native-sdk-sample) | ||
|
|
||
|
|
@@ -155,9 +151,9 @@ function MyComponent() { | |
| const [decision, isClientReady, didTimeout] = useDecision('the-flag'); | ||
| return ( | ||
| <React.Fragment> | ||
| { isClientReady && <div>The Component</div> } | ||
| { didTimeout && <div>Default Component</div>} | ||
| { /* If client is not ready and time out has not occured yet, do not render anything */ } | ||
| {isClientReady && <div>The Component</div>} | ||
| {didTimeout && <div>Default Component</div>} | ||
| {/* If client is not ready and time out has not occured yet, do not render anything */} | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
|
|
@@ -277,7 +273,7 @@ class MyComp extends React.Component { | |
| constructor(props) { | ||
| super(props); | ||
| const { optimizely } = this.props; | ||
| const decision = optimizely.decide('feat1'); | ||
| const decision = optimizely.decide('feat1'); | ||
|
|
||
| this.state = { | ||
| decision.enabled, | ||
|
|
@@ -298,9 +294,11 @@ const WrappedMyComponent = withOptimizely(MyComp); | |
| Any component under the `<OptimizelyProvider>` can access the Optimizely `ReactSDKClient` via the `OptimizelyContext` with `useContext`. | ||
|
|
||
| _arguments_ | ||
|
|
||
| - `OptimizelyContext : React.Context<OptimizelyContextInterface>` The Optimizely context initialized in a parent component (or App). | ||
|
|
||
| _returns_ | ||
|
|
||
| - Wrapped object: | ||
| - `optimizely : ReactSDKClient` The client object which was passed to the `OptimizelyProvider` | ||
| - `isServerSide : boolean` Value that was passed to the `OptimizelyProvider` | ||
|
|
@@ -321,34 +319,33 @@ function MyComponent() { | |
| }; | ||
| return ( | ||
| <> | ||
| { decision.enabled && <p>My feature is enabled</p> } | ||
| { !decision.enabled && <p>My feature is disabled</p> } | ||
| { decision.variationKey === 'control-variation' && <p>Current Variation</p> } | ||
| { decision.variationKey === 'experimental-variation' && <p>Better Variation</p> } | ||
| {decision.enabled && <p>My feature is enabled</p>} | ||
| {!decision.enabled && <p>My feature is disabled</p>} | ||
| {decision.variationKey === 'control-variation' && <p>Current Variation</p>} | ||
| {decision.variationKey === 'experimental-variation' && <p>Better Variation</p>} | ||
| <button onClick={onClick}>Sign Up!</button> | ||
| </> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ### Tracking | ||
|
|
||
| Use the built-in `useTrackEvent` hook to access the `track` method of optimizely instance | ||
|
|
||
| ```jsx | ||
| import { useTrackEvent } from '@optimizely/react-sdk'; | ||
|
|
||
| function SignupButton() { | ||
| const [track, clientReady, didTimeout] = useTrackEvent() | ||
| const [track, clientReady, didTimeout] = useTrackEvent(); | ||
|
|
||
| const handleClick = () => { | ||
| if(clientReady) { | ||
| track('signup-clicked') | ||
| if (clientReady) { | ||
| track('signup-clicked'); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <button onClick={handleClick}>Signup</button> | ||
| ) | ||
| return <button onClick={handleClick}>Signup</button>; | ||
| } | ||
| ``` | ||
|
|
||
|
|
@@ -411,70 +408,99 @@ To rollout or experiment on a feature by user rather than by random percentage, | |
|
|
||
| ## Server Side Rendering | ||
|
|
||
| Right now server side rendering is possible with a few caveats. | ||
| The React SDK supports server-side rendering (SSR). To generate synchronous decisions during SSR, you must pre-fetch the datafile and pass it to `createInstance`. Using `sdkKey` alone is not supported for SSR because it requires an asynchronous network call. | ||
|
|
||
| **Caveats** | ||
| ### Setup | ||
|
|
||
| 1. You must download the datafile manually and pass in via the `datafile` option. Can not use `sdkKey` to automatically download. | ||
| Fetch the datafile on the server, create an Optimizely instance, and wrap your app with `<OptimizelyProvider>`: | ||
|
|
||
| 2. Rendering of components must be completely synchronous (this is true for all server side rendering), thus the Optimizely SDK assumes that the optimizely client has been instantiated and fired it's `onReady` event already. | ||
| ```jsx | ||
| import { createInstance, OptimizelyProvider, useDecision } from '@optimizely/react-sdk'; | ||
|
|
||
| ### Setting up `<OptimizelyProvider>` | ||
| // Pre-fetched datafile (fetching mechanism depends on your framework) | ||
| const optimizelyClient = createInstance({ | ||
| datafile, // must be provided for SSR | ||
| sdkKey: 'YOUR_SDK_KEY' | ||
| }); | ||
|
|
||
| Similar to browser side rendering you will need to wrap your app (or portion of the app using Optimizely) in the `<OptimizelyProvider>` component. A new prop | ||
| `isServerSide` must be equal to true. | ||
| function MyComponent() { | ||
| const [decision] = useDecision('flag1'); | ||
| return decision.enabled ? <p>Feature enabled</p> : <p>Feature disabled</p>; | ||
| } | ||
|
|
||
| ```jsx | ||
| <OptimizelyProvider optimizely={optimizely} user={{ id: 'user1' }} isServerSide={true}> | ||
| <App /> | ||
| </OptimizelyProvider> | ||
| // Wrap your app with OptimizelyProvider | ||
| <OptimizelyProvider optimizely={optimizelyClient} user={{ id: 'user1' }} isServerSide={typeof window === 'undefined'}> | ||
| <MyComponent /> | ||
| </OptimizelyProvider>; | ||
| ``` | ||
|
|
||
| All other Optimizely components, such as `<OptimizelyFeature>` and `<OptimizelyExperiment>` can remain the same. | ||
| ### Configuring the instance for server use | ||
|
|
||
| ### Full example | ||
| Server-side instances are short-lived (created per request) and may not be garbage collected immediately. To avoid unnecessary background work and ensure events are dispatched before the instance is discarded, configure `createInstance` with server-appropriate options: | ||
|
|
||
| ```jsx | ||
| import * as React from 'react'; | ||
| import * as ReactDOMServer from 'react-dom/server'; | ||
| import { createInstance, OptimizelyDecideOption } from '@optimizely/react-sdk'; | ||
|
|
||
| import { | ||
| createInstance, | ||
| OptimizelyProvider, | ||
| useDecision, | ||
| } from '@optimizely/react-sdk'; | ||
| const isServer = typeof window === 'undefined'; | ||
|
|
||
| const fetch = require('node-fetch'); | ||
| const optimizelyClient = createInstance({ | ||
| datafile, | ||
| sdkKey: 'YOUR_SDK_KEY' | ||
| datafileOptions: { autoUpdate: !isServer }, | ||
| eventBatchSize: isServer ? 1 : 10, | ||
| eventMaxQueueSize: isServer ? 1 : 100, | ||
| // Optional: disable decision events on server if they will be sent from the client | ||
| defaultDecideOptions: isServer ? [OptimizelyDecideOption.DISABLE_DECISION_EVENT] : [], | ||
| }); | ||
| ``` | ||
|
|
||
| function MyComponent() { | ||
| const [decision] = useDecision('flag1'); | ||
| return ( | ||
| <React.Fragment> | ||
| { decision.enabled && <p>The feature is enabled</p> } | ||
| { !decision.enabled && <p>The feature is not enabled</p> } | ||
| { decision.variationKey === 'variation1' && <p>Variation 1</p> } | ||
| { decision.variationKey === 'variation2' && <p>Variation 2</p> } | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
| | Option | Server value | Why | | ||
| |---|---|---| | ||
| | `datafileOptions.autoUpdate` | `false` | No need to poll for datafile updates on a per-request instance | | ||
| | `eventBatchSize` | `1` | Flush events immediately — the instance won't live long enough for a batch to fill | | ||
| | `eventMaxQueueSize` | `1` | Prevent event accumulation in a short-lived instance | | ||
| | `defaultDecideOptions` | `[DISABLE_DECISION_EVENT]` | Optional — avoids duplicate decision events if the client will also fire them after hydration | | ||
|
|
||
| async function main() { | ||
| const resp = await fetch('https://cdn.optimizely.com/datafiles/<Your-SDK-Key>.json'); | ||
| const datafile = await resp.json(); | ||
| const optimizelyClient = createInstance({ | ||
| datafile, | ||
| ### React Server Components | ||
|
|
||
| The SDK can also be used directly in React Server Components without `OptimizelyProvider`. Create an instance, set the user, wait for readiness, and make decisions — all within an `async` server component: | ||
|
|
||
| ```tsx | ||
| import { createInstance } from '@optimizely/react-sdk'; | ||
|
|
||
| export default async function ServerExperiment() { | ||
| const client = createInstance({ | ||
| sdkKey: process.env.OPTIMIZELY_SDK_KEY || '', | ||
| }); | ||
|
|
||
| const output = ReactDOMServer.renderToString( | ||
| <OptimizelyProvider optimizely={optimizelyClient} user={{ id: 'user1' }} isServerSide={true}> | ||
| <MyComponent /> | ||
| </OptimizelyProvider> | ||
| ); | ||
| console.log('output', output); | ||
| client.setUser({ | ||
| id: 'user-123', | ||
| }); | ||
|
|
||
| await client.onReady(); | ||
|
|
||
| const decision = client.decide('flag-1'); | ||
|
|
||
| client.close(); | ||
|
|
||
| return decision.enabled | ||
| ? <h1>Experiment Variation</h1> | ||
| : <h1>Control</h1>; | ||
| } | ||
| main(); | ||
| ``` | ||
|
|
||
| ### Next.js Integration | ||
|
|
||
| For detailed Next.js examples covering both App Router and Pages Router patterns, see the [Next.js Integration Guide](docs/nextjs-integration.md). | ||
|
|
||
| ### Limitations | ||
|
|
||
| - **Datafile required** — SSR requires a pre-fetched datafile. Using `sdkKey` alone falls back to a failed decision. | ||
| - **Static user only** — User `Promise` is not supported during SSR. | ||
| - **ODP segments unavailable** — ODP audience segments require async I/O and are not available during server rendering. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since there is a change in the hooks.ts file, I assume we need a patch update. In that case, can we consider adding the option to pass pre-fetched qualifiedSegments as props to the release? |
||
|
|
||
| For more details and workarounds, see the [Next.js Integration Guide — Limitations](docs/nextjs-integration.md#limitations). | ||
|
|
||
| ## Disabled event dispatcher | ||
|
|
||
| To disable sending all events to Optimizely's results backend, use the `logOnlyEventDispatcher` when creating a client: | ||
|
|
||
Oops, something went wrong.
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.