Skip to content

New Recipe

Our branch protections will require a PR for now, but that PR will be approved without review by any maintainer.

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

  1. 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;
     }
    }
  2. Create a @/tests/config/featureFlag.test.ts file to test the FeatureFlag class:
    typescript
    import { 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");
     });
    });
  3. Create a @/platform/features.ts file to centralize feature enabling & access.
    typescript
    import { 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'];
  4. Next, go to Using Feature Flags to see how to use the feature flags in your code.
  5. Done!