Skip to content
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

Circular structure of SDK client breaks next.js SRR #16

Open
FilipStenbeck opened this issue Nov 21, 2019 · 4 comments
Open

Circular structure of SDK client breaks next.js SRR #16

FilipStenbeck opened this issue Nov 21, 2019 · 4 comments

Comments

@FilipStenbeck
Copy link

FilipStenbeck commented Nov 21, 2019

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> } }
@oMatej
Copy link

oMatej commented Jan 27, 2020

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
}

@HaNdTriX
Copy link

HaNdTriX commented Apr 29, 2020

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

  ...
}

@patrickgordon
Copy link

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.

@Tamara-Barum
Copy link

Internal Ticket [FSSDK-8658]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants