Appearance
How To Setup Feature Flags
THIS RECIPE IS A WORK IN PROGRESS
Goal: Initial, one-time setup of feature flags for a Typescript/React Native project.
Note: This implementation is for compile-time feature flags only. Changes to a flag’s state will require a new code deployment. This approach allows you to hide incomplete features in production while still deploying completed stories incrementally.
Steps
- Create a
@/config/featureFlag.ts
file to define the FeatureFlag class.typescript// This class will be used to handle how the feature flags are implemented throughout the codebase. // // It purposely only exposes the `executeCode` and `showComponent` methods to make sure that the // feature flags are used in a controlled manner. Also, we are only dealing with `enabled` and `disabled` // states for now, but additional states can be added as needed export type FlagState = "enabled" | "disabled"; export class FeatureFlag { private state: FlagState; private testOverride: FlagState | null = null; constructor(initialState: FlagState) { this.state = initialState; } executeCode<T>(options: { enabled: () => T; disabled: () => T; [key: string]: (() => T) | undefined }): T { return (options[this.getCurrentState()] ?? options.disabled)(); } showComponent<T>(components: { enabled: T; disabled: T; [key: string]: T | undefined }): T { return components[this.getCurrentState()] ?? components.disabled; } get forTest() { return { enable: () => (this.testOverride = "enabled" as FlagState), disable: () => (this.testOverride = "disabled" as FlagState), setState: (state: FlagState) => (this.testOverride = state), reset: () => (this.testOverride = null), }; } private getCurrentState(): FlagState { return this.testOverride ?? this.state; } }
- Create a
@/tests/config/featureFlag.test.ts
file to test theFeatureFlag
class:typescriptimport { Feature, FlagState } from "@/config/featureFlag"; describe("FeatureFlag Testing", () => { it("should execute the enabled code path", () => { let result = ""; new FeatureFlag("enabled").executeCode({ enabled: () => (result = "Enabled Path"), disabled: () => (result = "Disabled Path"), }); expect(result).toBe("Enabled Path"); }); it("should execute the disabled code path", () => { let result = ""; new FeatureFlag("disabled").executeCode({ enabled: () => (result = "Enabled Path"), disabled: () => (result = "Disabled Path"), }); expect(result).toBe("Disabled Path"); }); it("should execute the test-specified path when a test overrides", () => { let result = ""; const testSubject = new FeatureFlag("disabled"); testSubject.forTest().enable(); testSubject.executeCode({ enabled: () => (result = "Enabled Path"), disabled: () => (result = "Disabled Path"), }); expect(result).toBe("Enabled Path"); }); it("should default to the disabled path when there is no explicit handler for the state", () => { let result = ""; new FeatureFlag("future state" as unknown as FlagState).executeCode({ enabled: () => (result = "Enabled Path"), disabled: () => (result = "Disabled Path"), }); expect(result).toBe("Disabled Path"); }); it("should reset test overrides when asked", () => { let result = ""; const testSubject = new FeatureFlag("disabled"); testSubject.forTest().enable(); testSubject.forTest().reset(); testSubject.executeCode({ enabled: () => (result = "Enabled Path"), disabled: () => (result = "Disabled Path"), }); expect(result).toBe("Disabled Path"); }); });
- Create a
@/platform/features.ts
file to centralize feature enabling & access.typescriptimport { FeatureFlag } from "@/config/featureFlag.ts"; enum Flag { Enabled = new FeatureFlag("enabled"), Disabled = new FeatureFlag("disabled"), }; const prod_flags = { run_with_scissors: Flag.Enabled, jog_with_knives: Flag.Disabled, swim_with_sharks: Flag.Enabled, }; export type Features = typeof prod_flags; const beta_flags: Features = { ...prod_flags, swim_with_sharks: Flag.Disabled, }; const int_flags: Features = { ...beta_flags, }; const dev_flags: Features = { ...int_flags, }; const local_flags: Features = { ...dev_flags, jog_with_knives: Flag.Enabled, }; const flags: Record<string, Features> = { local: local_flags, dev: dev_flags, development: dev_flags, int: int_flags, integration: int_flags, beta: beta_flags, prod: prod_flags, production: prod_flags, }; export const Feature = flags[process.env.NODE_ENV.toLowerCase() || 'local'] ?? flags['prod'];
- Next, go to Using Feature Flags to see how to use the feature flags in your code.
- Done!