Frontend feature flagging

Viral Tagdiwala
4 min readAug 27, 2022

Frontend feature flags are pretty much a quintessential feature for companies who value Continous Delivery — one cannot realistically deploy at a high pace and also get parts entirely ready in time for frequent deployments. This is probably one of the places where feature flagging comes in handy.

Many other pain points, such as allowing the QA team access to specific features while hiding them from the users, etc are needed in many scenarios. Sure, there is a fairly large selection of SaaS products that allow controlled flags that can be managed via a dashboard, however, if your firm doesn’t need such comprehensive control over the flags or doesn’t wish to shell out subscription costs here is a quick and easy way to come up with feature flags that can be toggled on the fly via developers, QA folks and anyone else in the chain that may want to turn certain features on or off.

Our first order of business will be finding out the current environment our web app is running in — you can choose to use the method of checking your URL or any other business logic that returns strings specifying the environment in which your build is running.

export function getEnviornment() {
if (typeof window === 'undefined') {
return null
}
const { hostname } = window.locationif (hostname === PRODUCTION_URL) {
return 'production'
} else if (hostname === DEVELOPMENT_URL) {
return 'development'
} else if (
hostname === 'localhost' ||
hostname.startsWith('192.168')
) {
return 'local'
} else {
throw new Error('unknown environment')
}
}

This will come in handy when we wish to determine specific flags to be ON in local/development builds but OFF in production builds.

We now start off by writing our default state of flags and defining the env variable

const env = getEnviornment()const defaults = {
someFeatureFlag: env === 'local',
someOtherFeatureFlag: false
}

We wish to allow multiple methods of keeping features ON/OFF, we can either define this via env state or explicitly turn features on or off.

Next up, we wish to declare an object type of defaults/all possible feature flags that we have and have them stored as a hashmap with the key as the flag name and value as boolean.

We start off by using Object.defineProperty for immutability purposes and generate a Map object in the constructor of our feature flag class

type FeatureKey = keyof typeof defaultsclass FeatureFlags {
_current!: Map<string, boolean>
constructor(defaults: Record<string, boolean>) {
Object.defineProperty(this, '_current', {
enumerable: false,
value: new Map()
})
for (const key of Object.keys(defaults)) {
this._current.set(key, defaults[key])
Object.defineProperty(this, key, {
enumerable: true,
get: () => this._current.get(key),
set: (value: boolean) => this.set(key as FeatureKey, value)
})
}
this.load()
}
...
}

Hydrating Values

Next, up we need to write a hydration service that loads the keys from some storage medium & sets the current values. This storage medium can be your backend, some third-party service, or in this case, we’ll be using localStorage to set these values (this will come in handy later)

load() {
return Object.keys(this._current).forEach((key) => {
const stored = localStorage.getItem(`featureFlags.${key}`)
if (stored !== null) {
this._current.set(key, stored === 'true')
}
})
}

Setting/Unsetting values on the fly

Now, since we allow caching & hydrating values inside localStorage, we can technically allow any user (who knows the exact keys) to allow setting/unsetting values. This is extremely helpful to QA, as they can turn on certain features, even on the production build of your application to test functionality, while it stays hidden from actual customers.

set(key: FeatureKey, value = true) {
if (!this._current.has(key)) throw new RangeError('Invalid key')
if (value !== true && value !== false) throw new TypeError('Value must be boolean') localStorage.setItem(`featureFlags.${key}`,JSON.stringify(value))
}
unset(key: FeatureKey) {
if (!this._current.has(key)) throw new RangeError('Invalid key')
localStorage.removeItem(`featureFlags.${key}`)
}

Once we ensure the provided keys are legal, we proceed either setting/removing them from localStorage — Remember since we are interacting with the localStorage here any manipulations on this state require rehydrating the application feature flag state, which means reloading the app.

Checking the values in the code

Finally, in order to facilitate checking these values in the codebase, we add an ‘enabled’ function that fetches the key-value pairs and checks if a key is enabled/disabled.

enabled(key: FeatureKey): boolean {
if (!this._current.has(key)) throw new RangeError('Invalid key')
return Boolean(this._current.get(`${key}`))
}

Injecting this service to Window Object

In order to allow manipulations to take place via the console of your browser (devtools), we need to inject this into the window object. In order to do that, we use Object.assign()

export const features = new FeatureFlags(defaults)
export default features
// expose to devtools
if (typeof window !== 'undefined') {
Object.assign(window, { features })
}

Wrapping it up

In order to use this in your codebase, all you do is -

import features from 'features'export function SomeComponent():JSX.Element {  return (
...
{features.enabled('someFeatureFlag') && <SomeOtherComponent />}
)
}

Your test team can simply head on over to the console and type in features.unset(‘someFeatureFlag’) or just manually delete the entry of the feature flag and hit refresh to disable the flag on the fly!

--

--