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

RFC: NextJS Support #2

Open
JakeGinnivan opened this issue Nov 22, 2021 · 0 comments
Open

RFC: NextJS Support #2

JakeGinnivan opened this issue Nov 22, 2021 · 0 comments

Comments

@JakeGinnivan
Copy link
Member

Background

FeatureBoard's node/browser SDKs have been designed to work together but have different entrypoints. On the server you need to initialise the server SDK which maintains a full copy of the feature state tree, allowing the features for a request to be calculated without connecting to the FeatureBoard service.
We do this because it means FeatureBoard will not slow down your requests, and FeatureBoard also will not affect your server's reliability if we happen to have an issue.

The server looks something like this:

async function createServer() {
   const featureBoardConnection = await ServerConnection.init(...)

    app.use((req) => {
         req.featureboardClient = featureBoardConnection.request(['plan-medium', 'other-audiences'])
         // pass this to the <FeatureBoardProvider client={req.featureboardClient}>
    })
}

On the client you can use the useClient hook which takes care of initialising the sdk, and re-initialising it if the users audiences happen to change. The SDK is initialised for a single set of audiences to reduce data transfer and reduce the amount of information sent to the browser.

NextJS doesn't allow you to hook into these two parts of the request pipeline and instead works at a higher abstraction level. This means we need to extend our SDK to open up options to use our SDK with NextJS or make it transparently isomorphic.

Option 1 - Server side integration only

getServerSideProps is an isomorphic method, during a server side render it runs to get the props for _app. Then during a client side navigation nextjs will call a endpoint maintained by next starting with the _next prefix.

This means that if our SDK enforces usage of getServerSideProps the user now has to deal with a API call which MUST be made before all page navigations and locks usage into the Server Side Rendering approach or be forced to use SSG, which isn't great for dynamic sites.

None the less, will document in case I find workarounds for the drawbacks.

import React from 'react';

import { GetServerSideProps } from 'next';
import Layout from 'components/Layout';
import { AppProps } from 'next/app';
import { createClient } from '@featureboard/js-sdk';
import { FeatureBoardService, ServerConnection } from '@featureboard/node-sdk';
import { FeatureBoardProvider } from '@featureboard/react-sdk';

let serverConnection: ServerConnection;

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
  // Calculate the audiences from the request
  const audiences: string[] = [];

  if (!serverConnection) {
    if (!process.env.FEATUREBOARD_ENV_KEY) {
      throw new Error('FEATUREBOARD_ENV_KEY missing');
    }

    serverConnection = await FeatureBoardService.init(
      process.env.FEATUREBOARD_ENV_KEY,
      {
        updateStrategy: 'on-request'
      }
    );
  }

  return {
    props: {
      featureState: serverConnection.getEffectiveValues(audiences)
    }
  };
};

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <div className="bg-primary">
      <FeatureBoardProvider client={createClient(pageProps.featureState)}>
        <Layout>
          <Component {...pageProps} />
        </Layout>
      </FeatureBoardProvider>
    </div>
  );
}

To get this approach working, we need to extend our public API to include import { createClient } from '@featureboard/js-sdk', this simply creates a client based on an effective feature set.

Option 2 - Make the JS SDK Isomorphic

This is a more complex option.

Create 2 entrypoints browser / server. The node SDK optimisations would get moved into @featureboard/js-sdk/server as a seprate entrypoint, the exports from @featureboard/js-sdk would behave differently depending on if they are imported from the browser or the server.

The main export would become getClient(['user', 'audiences'])

On the server

When getClientAsync is called, if there is no SDK initialised it will start initialising the SDK. Once the SDK has fully initialised it will resolve the promise.

Multiple calls to getClientAsync will return the same promise instance.

Option 3 - Make the React SDK isomorphic

Create 2 versions of the hook, on the server it stores the reference in a global, on the client in a react ref. Or could simply have 2 entrypoints which import the same hook, but pass the current client down, or have a factory function so the initialisation can be inverted.

Option 4 - Use Document extension + useClient hook

A custom document doesn't have the same downsides as _app, we could wrap the

in the FeatureBoardProvider with the server version of the SDK after it has been initialised.

Then in _app we could use the useClient hook if there isn't already a client created with context. This would be stable from a hooks point of view because the client would never become set allowing us to ignore the rules of hooks for this instance.

References

https://nextjs.org/docs/advanced-features/custom-document
https://nextjs.org/docs/advanced-features/automatic-static-optimization (highlights downsides of particular functions)
vercel/next.js#13910
vercel/next.js#1117
vercel/next.js#2582

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

No branches or pull requests

1 participant