Server-side Rendering Integration With Flagship [SSR]

This guide explains you how to use Flagship with nextJS using SSR mode.

📘

Next.js is used for this example

For this example, we have used the Next.js framework.
If you need more examples, please contact us at [email protected]

How it works

In server-side rendering (SSR), content is rendered on the server for every page request.

During this process, we fetch all flags that target the visitor and pass them as props to the __app.js component. This data is then used to populate the FlagshipProvider component.

📘

Note

Once your app has been loaded in a browser, you can use the Flagship SDK in the same way as you would in a client-side rendering mode.


Implementation

📘

Setup a NextJS project

Please refer to this article link to set up a NextJS project.

Once your project has been set up, you will need to install and initialize the Flagship React SDK.

yarn add @flagship.io/react-sdk

Initialize Flagship SDK server side

You have to create a module named startFlagshipSDK.js in the root directory of our project.

This module contains a function called startFlagshipSDK() which is responsible for initializing and starting the Flagship SDK.

The intention is to invoke this function when the server starts up.

//startFlagshipSDK.js

// Importing required modules from the Flagship React SDK
const { DecisionMode, Flagship, FSSdkStatus } = require("@flagship.io/react-sdk");

// Function to start the Flagship SDK
function startFlagshipSDK() {
    // Check if the Flagship SDK has already been initialized
    if (Flagship.getStatus() && Flagship.getStatus() !== FSSdkStatus.SDK_NOT_INITIALIZED) {
        return; // If it has been initialized, return early
    }
    // Start the Flagship SDK with the provided environment ID and API key
    Flagship.start(process.env.NEXT_PUBLIC_ENV_ID, process.env.NEXT_PUBLIC_API_KEY, {
        fetchNow: false, // Do not fetch flags immediately
        decisionMode: DecisionMode.DECISION_API // set decision mode : DECISION_API or BUCKETING
    });
}

// Export the startFlagshipSDK function
module.exports = { startFlagshipSDK };

We will invoke the startFlagshipSDK() function from within our next.config.js file.

This file is executed whenever we run the build, start, or dev commands for our Next.js application.

As such, we can use it as a convenient location to start the Flagship SDK.

Here is the refactor of next.config.js:

//next.config.js

// Importing the startFlagshipSDK function from the startFlagshipSDK module
const { startFlagshipSDK } = require('./startFlagshipSDK');

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
};

// Exporting a function that returns the Next.js configuration object
module.exports = () => {
  // Check if the 'dev' or 'start' argument is present in the process arguments
  if (process.argv.includes('dev') || process.argv.includes('start')) {
    startFlagshipSDK(); // If it is, start the Flagship SDK
  }
  return nextConfig; // Return the Next.js configuration object
};

Initialize Flagship in the project

We will customize the next component App to initialize the Flagship SDK.

//pages/__page.jsx

import { FlagshipProvider } from '@flagship.io/react-sdk'
import '../styles/globals.css'


function MyApp({ Component, pageProps }) {
  const { initialVisitorData, initialFlagsData } = pageProps
  return (
    <FlagshipProvider
      envId={process.env.NEXT_PUBLIC_ENV_ID}
      apiKey={process.env.NEXT_PUBLIC_API_KEY}
      visitorData={initialVisitorData || {}}
      initialFlagsData={initialFlagsData}
    >
      <Component {...pageProps} />
    </FlagshipProvider>)

}

export default MyApp

Our MyApp component will have two props: initialVisitorData and initialFlagsData.

The initialVisitorData prop contains data about the visitor, while the initialFlagsData prop contains the FlagsData that was fetched from the getServerSideProps function during a page request.

This data is used to populate the FlagshipProvider component.

There is no need to initialize the Flagship SDK again in the getServerSideProps function, as it has already been initialized when our Next.js application started up. Instead, we can simply create a visitor and fetch the relevant flags.

Here is the index page:

// pages/index.jsx

// Importing required modules from the Flagship React SDK
import {
  useFsFlag,
  useFlagship,
  Flagship,
  HitType,
  EventCategory,
} from "@flagship.io/react-sdk";
import styles from "../styles/Home.module.css";

// Home page component
export default function Home() {
  // Get the Flagship instance
  const fs = useFlagship();

  // Get the value of the 'my_flag_key' flag
  const myFlag = useFsFlag("my_flag_key");

  // Function to send a hit to Flagship
  const onSendHitClick = () => {
    fs.sendHits({
      type: HitType.EVENT,
      category: EventCategory.ACTION_TRACKING,
      action: "click button",
    });
  };

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1>Next ServerSide Rendering Integration With Flagship [SSR]</h1>
        <p>flag key: my_flag_key</p>
        <p>flag value: {myFlag.getValue("default-value")}</p>
        <button
          style={{ width: 100, height: 50 }}
          onClick={() => {
            onSendHitClick();
          }}
        >
          Send hits
        </button>
      </main>
    </div>
  );
}

// This function runs only on the server side
export async function getServerSideProps(context) {
  const { res, req } = context;

  // Get the visitor ID from the 'fs_visitorID_cookie' cookie if it exists
  const fs_visitorID_cookie = req.cookies["fs_visitorID_cookie"];

  // Define initial visitor data
  const initialVisitorData = {
    id: fs_visitorID_cookie, // If the cookie does not exist, the SDK will generate a new visitor ID
    hasConsented: true,
    context: {
      any: "value",
    },
  };

  // Create a new visitor using the initial visitor data
  const visitor = Flagship.newVisitor({
    visitorId: initialVisitorData.id,
    context: initialVisitorData.context,
    hasConsented: initialVisitorData.hasConsented
  });

  // Fetch flags for the visitor
  await visitor.fetchFlags();

  // Set the 'fs_visitorID_cookie' cookie with the visitor ID
  res.setHeader("Set-Cookie", `fs_visitorID_cookie=${visitor.visitorId}`);

  // Pass data to the page via props
  return {
    props: {
      initialFlagsData: visitor.getFlags().toJSON(),
      initialVisitorData: {
        ...initialVisitorData,
        id: visitor.visitorId,
      },
    },
  };
}

In the getServerSideProps of the index component page, we :

  • fetch the flags for the targeted visitor
  • pass the fetched flags to the page via props

Alternate Approach to Implementing Bucketing Mode

We have created a Promise that starts the SDK in Bucketing mode. The Promise is resolved once the first bucketing file request call responds, even if the request fails. This is because the bucketing polling has already started, so another bucketing request will be performed in 10 seconds.

//startFlagshipSDK.js

// Function to start the Flagship SDK
function startFlagshipSDKAsync() {
    // Return a new Promise
    return new Promise((resolve) => {
        // Check if the Flagship SDK has already been initialized
        if (
            Flagship.getStatus() &&
            Flagship.getStatus() !== FSSdkStatus.SDK_NOT_INITIALIZED
        ) {
            // If it has been initialized, resolve the Promise with the Flagship object and return early
            resolve(Flagship);
            return;
        }
        // If the SDK has not been initialized, start it with the specified parameters
        Flagship.start(
            process.env.NEXT_PUBLIC_ENV_ID, // Environment ID
            process.env.NEXT_PUBLIC_API_KEY, // API key
            {
                pollingInterval: 10, // Set polling interval to 10
                fetchNow: false, // Do not fetch flags immediately
                decisionMode: DecisionMode.BUCKETING, // Set decision mode to BUCKETING
                onBucketingSuccess: () => {
                    // It is triggered when the first bucketing file request succeeds, resolve the Promise with the Flagship object
                    resolve(Flagship);
                },
                onBucketingFail() {
                    // It is triggered when the first bucketing file request fails, resolve the Promise with the Flagship object
                    resolve(Flagship);
                },
            }
        );
    });
}

We have refactored the index.jsx module. The startFlagshipSDKAsync function will no longer be called in the next.config.js file. Instead, it will be called on each page in the getServerSideProps function.

// pages/index.jsx

// Importing required modules from the Flagship React SDK
import {
  useFsFlag,
  useFlagship,
  HitType,
  EventCategory,
} from "@flagship.io/react-sdk";
import styles from "../styles/Home.module.css";
import { startFlagshipSDKAsync } from "../startFlagshipSDK"; // Importing startFlagshipSDKAsync function

// Home page component
export default function Home() {
  // Get the Flagship instance
  const fs = useFlagship();

  // Get the value of the 'my_flag_key' flag
  const myFlag = useFsFlag("my_flag_key");

  // Function to send a hit to Flagship
  const onSendHitClick = () => {
    fs.hit.sendHits({
      type: HitType.EVENT,
      category: EventCategory.ACTION_TRACKING,
      action: "click button",
    });
  };

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1>Next ServerSide Rendering Integration With Flagship [SSR]</h1>
        <p>flag key: my_flag_key</p>
        <p>flag value: {myFlag.getValue("default-value")}</p>
        <button
          style={{ width: 100, height: 50 }}
          onClick={() => {
            onSendHitClick();
          }}
        >
          Send hits
        </button>
      </main>
    </div>
  );
}

// This function runs only on the server side
export async function getServerSideProps(context) {
  const { res, req } = context;

  // Get the visitor ID from the 'fs_visitorID_cookie' cookie if it exists
  const fs_visitorID_cookie = req.cookies["fs_visitorID_cookie"];

  // Define initial visitor data
  const initialVisitorData = {
    id: fs_visitorID_cookie, // If the cookie does not exist, the SDK will generate a new visitor ID
    hasConsented: true,
    context: {
      any: "value",
    },
  };

  // start the SDK et get the Flagship instance
  const flagship = await startFlagshipSDKAsync();

  // Create a new visitor using the initial visitor data
  const visitor = flagship.newVisitor({
    visitorId: initialVisitorData.id,
    context: initialVisitorData.context,
    hasConsented: initialVisitorData.hasConsented
  });

  // Fetch flags for the visitor
  await visitor.fetchFlags();

  // Set the 'fs_visitorID_cookie' cookie with the visitor ID
  res.setHeader("Set-Cookie", `fs_visitorID_cookie=${visitor.visitorId}`);

  // Pass data to the page via props
  return {
    props: {
      initialFlagsData: visitor.getFlags().toJSON(),
      initialVisitorData: {
        ...initialVisitorData,
        id: visitor.visitorId,
      },
    },
  };
}

🚧

The initial request may take a little longer to process, but subsequent requests will be processed almost instantly.

Do you have any feedback or suggestions? Would you like to see another tutorial? Contact us with your thoughts [email protected].

You can view the full code for this example on github