Add Feature Flags to your NextJS App Using Firebase’s Realtime Database

Add Feature Flags to your NextJS App Using Firebase’s Realtime Database

·

5 min read

Featured on Hashnode

If you regularly deploy code to multiple environments, you may have considered implementing feature flags. A feature flag is a way to turn on or turn off a particular feature or implementation — without using code.

For example, you may want new users who register to your app to verify their email before they are given access to certain pages. However, you don’t necessarily want that to be the case on your staging/QA environment, and certainly not the case on your local/dev environment.

Or, you may only want to show the user certain menu options after they’ve onboarded. The development and testing experience would be quite cumbersome if developers had to go through an entire onboarding process every time a new user was created. One way to deal with these environment-specific features is by simply hardcoding rules. Something like this:

const isDevelopment = process.env.NODE_ENV === 'development';
isDevelopment ? showMenu() : checkOnboardingStatus();

Oftentimes this does the trick. But what if you want the ability to toggle the flag without changing the code? What if suddenly you want the staging environment to force new users to go through the onboarding flow before accessing the site, but you don’t want to enforce it on the development environment? Or maybe the client wants to “try” a new feature for a set time, and wants the ability to do so without dev work?

This is where feature flags come in.

There are some services out there that offer feature flag integrations at a cost, but you can actually set it up for free using Firebase’s realtime database. Showing you how to get set up on Firebase is beyond the scope of this article but I’ll show you how it works on the client side.

Firstly, the Firebase Realtime Database is basically an object-like data structure, and looks something like this:

feature-flags: 
  forceOnboarding: false,
  stitchOnboarding: true, 
  mollieEnabled: false, 
  stitchTesting: true,
 ...

Then comes fetching the data.

There are different ways you might want to consider fetching the data, but for my purposes I decided to create a custom hook and fetch with useSWR, which basically fetches and caches your result. useSWR (stale-while-revalidate) is created by the people of Vercel and “is a strategy to first return the data from cache (stale), then send the fetch request (revalidate), and finally come with the up-to-date data.” So basically useSWR will cache that first fetch result and always return the cached result until the fetch result changes.

This is what it looks like in a custom hook with Typescript:

import { useState } from 'react';
import { useEffect } from 'react';
import useSWR from 'swr';

const FIREBASE_ENDPOINT = '/api/firebase'; //this endpoint handles the firebase integration for each NODE_ENV

const fetcher = async () => {
  const res = await fetch(FIREBASE_ENDPOINT);
  const data = await res.json();

  if (res.status !== 200) {
     throw new Error(data.message);
  }
  return data;
};

interface IFeatureFlags {
  forceOnboarding: boolean;
  stitchOnboarding: boolean;
  stitchTesting: boolean;
  mollieEnabled: boolean;
}

export function useFeatureFlag() {
  const [featureFlags, setFeatureFlags] =   useState<IFeatureFlags>(null);
  const { data, error } = useSWR(FIREBASE_ENDPOINT, fetcher);
  const loading = !data && !error;
  useEffect(() => {
    if (featureFlags) {
     return;
    }
  data?.data && setFeatureFlags(data.data);
  }, [featureFlags, data]);

 return { featureFlags, error, loading };

}

Why a custom hook? Well, for one, it encapsulates the logic all in one place, keeping the code clean and making the hook modular. Also, it gives us the freedom to only call the Firebase endpoint when components or pages containing the useFeatureFlags() hook render. And when multiple components use the hook, useSWR will first check the cache for the data and return while revalidating the data, avoiding unnecessary re-renders.

Using the data

Now that we’re getting our flags from Firebase, we have to use the data. And there’s two ways I like to do so, depending on the desired functionality.

One way is to simply get the data from the custom hook and tell your app to behave a certain way based on that data. So for example, if you need to re-route the user depending on the flag, it might look like this:

...
const MyOrdersPage = () => {
  const { featureFlags } = useFeatureFlag();
  const isUserOnboarded = data?.profile?.activationStatus === 
    'COMPLETED';

useEffect(() => {
   if (isUserOnboarded) {
      return;
   }
   if (data && featureFlags) {
      featureFlags[FEATURE_FLAGS.FORCE_ONBOARDING] &&
      outer.push('/dashboard');
   }
}, [featureFlags, Router, isUserOnboarded, data]);
...

The second use case would be to show or hide a particular feature — maybe a button or advertisement, or an entire integration. The way I like to do this is by creating a FeatureFlag component which would wrap the feature you want to be able to toggle.

It would look like this:

export enum FEATURE_FLAGS {
  FORCE_ONBOARDING = 'forceOnboarding',
  STITCH_ONBOARDING = 'stitchOnboarding',
  STITCH_TESTING = 'stitchTesting',
  MOLLIE_ENABLED = 'mollieEnabled',
}

To keep code clean, I like to work with a constant file. And then I wrap my feature with the <FeatureFlag /> component

<FeatureFlag featureKey={FEATURE_FLAGS.MOLLIE_ENABLED}>
  <Button onClick={() => handleMollieFlow()}>
     Pay With Mollie    
  </Button>
</FeatureFlag>

In theory you can also import the useFeatureFlag() hook directly in this component and conditionally render the Button, but I find this method to be cleaner and more readable, since all the logic happens in the FeatureFlag component:

import { useMemo } from 'react';
import { is } from 'ramda';
import { useFeatureFlag } from '@icepik/src/hooks/useFeatureFlag';

interface IFeatureFlag {
  children: any;
  featureKey: string;
  activeWhenDisabled?: boolean;
}

const FeatureFlag = ({ children, featureKey = '', activeWhenDisabled = false}: IFeatureFlags) => {
  const { featureFlags } = useFeatureFlag();
  const isFeatureActive = useMemo(() => {
  const activeFeatureKeys =  featureFlags ?
          Object.keys(featureFlags).filter(flag =>
          !!featureFlags[flag]) : [];
    return activeFeatureKeys.includes(featureKey);
  }, [featureKey, featureFlags]);

  const renderChildren = () => {
    // This only conditionally renders children, and can improve
      performance

    if (is(Function, children)) {
       return children()
     };
  return children;
  };

  if (activeWhenDisabled) {
   return isFeatureActive ? null : renderChildren();
  }

  return isFeatureActive ? renderChildren() : null;
};

export default FeatureFlag;

Here are a couple of final observations:

  • I’m using Ramda here. It’s not necessary, but it’s a really cool functional library that uses currying and never mutates data.
  • I included an optional prop of isActiveWhenDisabled which will render the component when the featureFlag is set to false.
  • I included a small performance hack that checks if you’re passing a function instead of a component.

There’s many other ways of integrating feature flags into your application but this is a free and fairly simple way of getting started with multiple environments. If you’ve discovered hacks or products that have simplified the process for you, feel free to leave a comment below!