Circular structure of SDK client breaks next.js SRR
We get a SDK client on the server by running createInstance({...})
Next.js serialize all objects to json when sending them to the client. We then get
Error: Circular structure in "getInitialProps" result of page "/_error". https://err.sh/zeit/next.js/circular-structure
The reason is that the SDKClient has a circular structure whitch is not supported in JSON
TypeError: Converting circular structure to JSON
Creating on client at the server side and the create it again in the client browser leads to a flickering behavior, since we recreate the SDK client and the turned on feature temporary goes away until we load data in the newly created client
It would be great if you could you change your SDK client to not use circular structure, so it can be serialized.
Our created SDK client looks like this
OptimizelyReactSDKClient {
user: { id: null, attributes: {} },
isUserPromiseResolved: false,
onUserUpdateHandlers: [],
initialConfig: { sdkKey: 'DNNv456Qy6tj6TuosxyyvE' },
userPromiseResovler: [Function],
_client:
Optimizely {
clientEngine: 'react-sdk',
clientVersion: '1.0.1',
errorHandler: NoopErrorHandler {},
eventDispatcher: { dispatchEvent: [Function: dispatchEvent] },
__isOptimizelyConfigValid: true,
logger: OptimizelyLogger { messagePrefix: '' },
projectConfigManager:
ProjectConfigManager {
__updateListeners: [Array],
jsonSchemaValidator: [Object],
skipJSONValidation: false,
__configObj: null,
datafileManager: [NodeDatafileManager],
__readyPromise: [Promise] },
__disposeOnUpdate: [Function: bound ],
__readyPromise: Promise { <pending> },
decisionService:
DecisionService {
audienceEvaluator: [AudienceEvaluator],
forcedVariationMap: {},
logger: [OptimizelyLogger],
userProfileService: null },
notificationCenter:
NotificationCenter {
logger: [OptimizelyLogger],
errorHandler: NoopErrorHandler {},
__notificationListeners: [Object],
__listenerId: 1 },
eventProcessor:
LogTierV1EventProcessor {
dispatcher: [Object],
queue: [DefaultEventQueue],
notificationCenter: [NotificationCenter] },
__readyTimeouts: { '0': [Object] },
__nextReadyTimeoutId: 1 },
userPromise: Promise { <pending> },
dataReadyPromise: Promise { <pending> } }
I do not think you should return the SDK client from getInitialProps method. In my opinion getInitialProps should return only datafile object and user attributes. Initialization of the SDK client should be done similarly to what you have in with-redux example: https://github.com/zeit/next.js/blob/canary/examples/with-redux/lib/redux.js
So basically you would end up with something like this:
let optimizelyServerInstance;
const getOrInitializeOptimizely = ({
sdkKey = OPTIMIZELY_SDK_KEY,
datafile
}) => {
if (typeof window === 'undefined') {
/**
* Always make a new Optimizely instance if server, otherwise instance with user details will be shared between requests
*/
return createInstance({
datafile,
datafileOptions: { autoUpdate: false }
});
}
if (!optimizelyServerInstance) {
/**
* Create Optimizely instance if unavailable on the client and set it on the window object
*/
optimizelyServerInstance = createInstance({
sdkKey,
datafile,
datafileOptions: {
autoUpdate: true,
updateInterval: UPDATE_INTERVAL
}
});
}
return optimizelyServerInstance;
};
export const withOptimizely = (PageComponent, { ssr = true } = {}) => {
const WithOptimizely = ({ datafile, user, ...props }) => {
const optimizely = getOrInitializeOptimizely({ datafile })
return (
<OptimizelyProvider
optimizely={optimizely}
user={user}
isServerSide={typeof window === 'undefined'}
>
<PageComponent {...props} />
</OptimizelyProvider>
)
}
// Make sure people don't use this HOC on _app.js level
if (process.env.NODE_ENV !== 'production') {
const isAppHoc =
PageComponent === App || PageComponent.prototype instanceof App
if (isAppHoc) {
throw new Error('The withOptimizely HOC only works with PageComponents')
}
}
// Set the correct displayName in development
if (process.env.NODE_ENV !== 'production') {
const displayName =
PageComponent.displayName || PageComponent.name || 'Component'
WithOptimizely.displayName = `withOptimizely(${displayName})`
}
if (ssr || PageComponent.getInitialProps) {
WithOptimizely.getInitialProps = async context => {
const datafile = getOptimizelyDatafile()
const user = getOptimizelyUserAttributes()
// Run getInitialProps from HOCed PageComponent
const pageProps =
typeof PageComponent.getInitialProps === 'function'
? await PageComponent.getInitialProps(context)
: {}
// Pass props to PageComponent
return {
...pageProps,
datafile,
user
}
}
}
return WithOptimizely
}
Sometimes you might want to return the SDK in getInitialProps. I used this pattern in withApollo before.
Otherwise you might need to initialize the optimizely client multible times on the server (1 x for getInitialProps phase and 1 x for rendering phase.)
But there is a trick that allows you to reuse the instance on the server:
// As soon as Next.js tries to serialize the client it will return null
optimizelyClientInstance = optimizely.createInstance()
optimizelyClientInstance.toJSON = () => null
return {
optimizelyClientInstance,
...pageProps
}
So you can now return the client inside getInitialProps and reuse it on the server.
const WithOptimizely = ({ optimizelyClientInstance = optimizely.createInstance(), ...props }) => {
...
}
We have the same issue.
We are trying to use Optimizely Rollouts with Segment to get Experiment Viewed events for free. In order to do so, there needs to be a object on the window called optimizelyClientInstance.
If we initialise this on the client -- it's too late, Segment has already loaded.
I attempted to serialize the instance that we created on the server-side and pass it down on the window as a script. I believe this should resolve our issues, but alas, due to this circular structure we cannot.
Internal Ticket [FSSDK-8658]