Introduction
You have probably used a product where a friend or a news article reports that a particular feature or UI has been rolled out and you are wondering when this particular update gets to you. This experience is made possible by what software engineers refer to as Feature Flags (or Feature Toggles).
Feature flags (or toggles) are the means by which software engineers control the visibility of features in their application while it is still undergoing testing or development before they eventually release it for use by general public.
Benefits of using Feature Flags
There are various reasons why you might need features flags in your application.
You might be working on a feature and you don’t want to delete the code you are currently working on or you want to collate enough data from your users before you finally release the feature in production. Feature flags allow you to create a phased or gradual roll out of features. This also provides you with a kill-switch in case you need to quickly disable a feature that is causing a bug in production.
Other times, you work in a team that releases in a weekly or bi-weekly cycle, you are working on a feature that is going to take longer than your release cycle and you don’t want to keep working on a branch that is going to be behind the main branch, doing so makes you rack up technical debt that increases the time to market of the feature because you are trying to fix merge conflicts.
Best Practices
While feature flags provide a way for you to experiment and ship quickly, there are some practices we must adhere to:
- Make sure that your flags have an expiration date. Having a long list of features behind a flag in production results in writing code that is built on a shaky foundation. There is no way we can build features that are completely isolated from one another. When you eventually turn a feature off, it is possible that it causes a bug in production.
- Make sure there is a one-to-one mapping from a flag to a feature.
- Have a centralized system for keeping track of your flags. Some companies use config files, YAML, or some times a dedicated table in their database.
Most companies use different solutions to help them create and manage feature flags especially when you are working in a company that has a lot of teams. These solutions also offer things like analytics or toggling for a specific number of users, essentially granular control over a large dataset.
In this article, we are going to implement a small scale implementation of feature flagging in React using the Context API that you can deploy for personal use or in your engineering team.
Implementation
Feature flags and Artificial Intelligence have one thing in common, at their core, you will find an if-else statement. jk. But this is what the framework of a feature flag handler looks like.
if (feature_is_enabled) {
// do new_thing;
} else {
// do current_thing;
}
To successfully build a feature flag manager you need:
- some place to keep or store your feature flags and
- an handler to check if the feature is enabled.
- a fallback in case you are unable to access the feature flag configuration.
We are going to build a feature flag that will allow us to toggle the login page UI of our application.
Storing Feature Flags
Since we just want a simple setup, we are going to store our feature flags in our environment variables and access them in a configuration file. This also allow us to toggle the status without having to commit new code to our repository, we just need to redeploy our application.
// .env.local
REACT_APP_NEW_LOGIN_PAGE=true
Then we can acccess it like this in our config file.
const config = { flags: { newLoginPage: process.env.REACT_APP_NEW_LOGIN_PAGE === "true", }, }; export default config;
Feature Flag Handler
Now that we can access our feature flags, we need to create an handler that checks if the feature is enabled using React Context API.
We are going to declare a type for our feature flag and create a context provider that will provide the feature flags to the application.
import { FC, createContext, ReactNode, useContext } from "react"; import config from "../config"; interface FeatureFlags { [key: string]: boolean; } interface FeatureFlagContextProps { featureFlags: FeatureFlags; } const FeatureFlagContext = createContext<FeatureFlagContextProps | undefined>( undefined ); interface FeatureFlagProviderProps { children: ReactNode; } export const FeatureFlagProvider: FC<FeatureFlagProviderProps> = ({ children, }) => { const featureFlags: FeatureFlags = config.flags; return ( <FeatureFlagContext.Provider value={{ featureFlags }}> {children} </FeatureFlagContext.Provider> ); };
Then next step is to wrap our app with the provider. We can do that either by wrapping our App
directly or creating a layout.tsx
file and wrapping our App
with that.
import "./styles.css"; import Layout from "./components/layout"; import OldLoginPage from "./components/old-login-page"; export default function App(): JSX.Element { return ( <Layout> <OldLoginPage /> </Layout> ); }
We can chose to leave the provider like this and import FeatureFlagContext
for use in our pages or components.
But we can also create a custom hook for easier use.
import { FC, createContext, ReactNode, useContext } from "react"; import config from "../config"; interface FeatureFlagContextProps { featureFlags: FeatureFlags; } type FeatureFlags = typeof config.flags; export type FeatureFlagName = keyof FeatureFlags; const FeatureFlagContext = createContext<FeatureFlagContextProps | undefined>( undefined ); interface FeatureFlagProviderProps { children: ReactNode; } export const FeatureFlagProvider: FC<FeatureFlagProviderProps> = ({ children, }) => { const featureFlags: FeatureFlags = config.flags; return ( <FeatureFlagContext.Provider value={{ featureFlags }}> {children} </FeatureFlagContext.Provider> ); }; export const useFeatureFlag = () => { const context = useContext(FeatureFlagContext); if (context === undefined) { throw new Error("useFeatureFlag must be used within a FeatureFlagProvider"); } return context; }; // you can also have this helper function export const useIsFeatureFlagEnabled = (flag: FeatureFlagName): boolean => { const context = useContext(FeatureFlagContext); if (context === undefined) { throw new Error( "useIsFeatureFlag must be used within a FeatureFlagProvider" ); } return context.featureFlags[flag] ?? false; };
The first custom hook is helpful when you are working in a file that needs to access a number of feature flags, which I will advise against. The second custom hook allows us to check if a feature is enabled.
Using the Feature Flag Handler
At this stage, we have a way to access our feature flags and a way to check if a feature is enabled. We can use the feature flag handler in our components like this.
import { FC, createContext, ReactNode, useContext } from "react"; import config from "../config"; interface FeatureFlagContextProps { featureFlags: FeatureFlags; } type FeatureFlags = typeof config.flags; export type FeatureFlagName = keyof FeatureFlags; const FeatureFlagContext = createContext<FeatureFlagContextProps | undefined>( undefined ); interface FeatureFlagProviderProps { children: ReactNode; } export const FeatureFlagProvider: FC<FeatureFlagProviderProps> = ({ children, }) => { const featureFlags: FeatureFlags = config.flags; return ( <FeatureFlagContext.Provider value={{ featureFlags }}> {children} </FeatureFlagContext.Provider> ); }; export const useFeatureFlag = () => { const context = useContext(FeatureFlagContext); if (context === undefined) { throw new Error("useFeatureFlag must be used within a FeatureFlagProvider"); } return context; }; // you can also have this helper function export const useIsFeatureFlagEnabled = (flag: FeatureFlagName): boolean => { const context = useContext(FeatureFlagContext); if (context === undefined) { throw new Error( "useIsFeatureFlag must be used within a FeatureFlagProvider" ); } return context.featureFlags[flag] ?? false; };
We can either stop here or take this a step further and create a FeatureFlag
wrapper component that handles the rendering of the feature's component and the fallback depending on the feature flag.
Since Sandpack doesn't support environment variables, we will need to update a config.ts
to true for simulation purposes.
const config = { flags: { newLoginPage: true, // newLoginPage: process.env.REACT_APP_NEW_LOGIN_PAGE === "true", }, }; export default config;
And that's it! We have a simple feature flag handler that we can use in our application with support for fallback and multiple feature flags.
Testing
It is very crucial that you have a way to test your feature flags. So that you can be sure your application is working as expected under different configuratiions.
// App.test.tsx
import { render, screen } from "@testing-library/react";
import Login from "./components/old-login-page";
import FeatureFlagProvider from "./context/featureFlagContext";
jest.mock("../src/config", () => ({
flags: {
newLoginPage: true,
},
}));
describe("App with feature flags", () => {
it("renders new login page when flag is enabled", () => {
render(
<FeatureFlagProvider>
<Login />
</FeatureFlagProvider>
);
const newLoginPage = screen.getByTestId("new-login-page");
expect(newLoginPage).toBeInTheDocument();
});
it("renders old login page when flag is disabled", () => {
const config = require("../src/config");
config.flags.newLoginPage = false;
render(
<FeatureFlagProvider>
<Login />
</FeatureFlagProvider>
);
const oldLoginPage = screen.getByTestId("old-login-page");
expect(oldLoginPage).toBeInTheDocument();
});
});
The above is a very simple example of how you can perform unit testing on your feature flag component. You can also extend this setup to perform integration testing as well.
You can check out the full working implementation of this code on GitHub.
Limitations
Since this is a very simple implementation, it has some limitations. An example is that you can't have a feature flag that is enabled for a (specific) group of users. Our flags are enabled or disabled for all users. To fix this, you can implement a roll out mechanism that enables a feature flag for a group of users using their attributes/traits.
Conclusion
We've explored how feaature flags can be a powerful way to introduce new feature into your application without compromising on the delivery of your existing features. Their benefits, best practices and how to implement a light weight feature flag handler with React Context in a React application with type safety and support for a fallback.
The implementation above can be improved upon and should in case you are building a large application, you can use a feature flag management tool like LaunchDarkly, Optimizely or Flagsmith to manage your feature flags as they allow for targeting, analytics and remote configurations.
Remember to follow best practices such as keeping your flags short-lived, maintaining a one-to-one mapping between flags and features, and regularly cleaning up unused flags.
Extra Resources
If you want to learn more about feature flags, here are some resources that you can check out:
- Feature Toggles (aka Feature Flags) on Martin Fowler
- Feature Flags by Feature Flags
Bonus Tips
For performance reasons, you can:
-
wrap the children component in a Suspense component.
import FeatureFlag from "../components/featureFlag"; const NewFeature = lazy(() => import("./NewFeature")); function MyComponent() { return ( <FeatureFlag flag="newFeature"> <Suspense fallback={<div>Loading...</div>}> <NewFeature /> </Suspense> </FeatureFlag> ); }
-
use the
useMemo
hook to memoize the feature flags component to reduce the number of re-renders.
Thanks to Abdulazeez Abdulazeez for reading initial drafts of this.