react-sdk icon indicating copy to clipboard operation
react-sdk copied to clipboard

Circular structure of SDK client breaks next.js SRR

Open FilipStenbeck opened this issue 6 years ago • 4 comments

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> } }

FilipStenbeck avatar Nov 21 '19 13:11 FilipStenbeck

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
}

oMatej avatar Jan 27 '20 08:01 oMatej

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 }) => {

  ...
}

HaNdTriX avatar Apr 29 '20 13:04 HaNdTriX

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.

patrickgordon avatar Aug 18 '20 01:08 patrickgordon

Internal Ticket [FSSDK-8658]

Tamara-Barum avatar Jul 19 '23 15:07 Tamara-Barum