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 Use Feature Flags

THIS RECIPE IS A WORK IN PROGRESS

Goal: Learn how to use feature flags in your Typescript/React Native project to conditionally include or exclude functionality at compile-time.

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. Complete Initial Setup for Feature Flags before proceeding.
  2. Unless you are re-using an existing flag, add your new feature flag to the @/platform/features.ts file. Add it to the prod_flags as Flag.Disabled, and to dev_flags (or a similar env of your choice) as Flags.Enabled.
  3. Based on the value of the feature flag, do you want to change the behavior of code or alter the rendering of a component?
    behavior:
    1. Accomplish the goal Use Feature Flag to Alter Code Behavior
    rendering:
    1. Accomplish the goal Use Feature Flag to Change Rendering for a Component
  4. Done!

Use Feature Flag to Alter Code Behavior

  1. Accomplish the goal Isolate the code that is changing
  2. Accomplish the goal Split feature-invariant from feature-dependent behaviors in tests
  3. Accomplish the goal Introduce a place to later add the new behavior
  4. Accomplish the goal Test-drive the new behavior
  5. Done!

Isolate the code that is changing

  1. Identify the section of code that you would like to alter.
    typescript
    function something() {
     const arbitrary = code();
     // start of code I want to change
     let i = 2 + 2;
     if(arbitrary) i = i + 3;
     // end of code I want to change
     return `nice ${i}`;
    }
  2. Use Extract Method on that section of code and name it [whatever]_beforeFeature[X], where [whatever] describes the purpose of the function, and [X] is the name of the feature used in the flag.
    typescript
    function something() {
     const arbitrary = code();
     const i = computeI_beforeFeatureFuzzyLogic(arbitrary);
     return `nice ${i}`;
    }
    function computeI_beforeFeatureFuzzyLogic (arbitrary: unknown): int {
     let i = 2 + 2;
     if(arbitrary) i = i + 3;
     return i;
    }
  3. Done!

Split feature-invariant from feature-dependent behaviors in tests

  1. Add 2 new empty sub-describe sections to the test for the outer method. These will hold the tests for the old & new behaviors. Name them before feature [X] and after feature [X].
    typescript
    describe("original thing", () => {
     it("first test", () => {});
     // ...
     describe("before feature FuzzyLogic", () => {
     });
     describe("after feature FuzzyLogic", () => {
     });
    });
  2. Add a beforeEach and afterEach to the outer describe.
    typescript
    beforeEach(() => {
     Feature.X.forTest().enable();
    });
    afterEach(() => {
     Feature.X.forTest().reset();
    });
  3. commit.
  4. Go through each unit test for the outer method (something in our example). For each test:
  5. Does that test verify logic / depend on the stuff inside the method you extracted, outside it, or both?
    inside:
    1. Change the test to call your new function ([whatever]_beforeFeature[X]) instead of the outer function.
    2. Make sure it passes, then commit.
    3. Cut this test and paste it into both new sub-describes.
    4. commit.
    outside:
    1. Go on to the next test; this test will not be changed by the feature flag's state. And your tests will ensure that it does not change.
    both:
    1. Note: This test is clearly verifying multiple behaviors, because it verifies a behavior that is changed by the new feature and one that is not. So we will make 2 tests, one for inside and one for outside. If this fails, then we will use the test overrides on feature flag, but that's a last resort. We would far rather split the test into one per behavior.
    2. make sure you have no local changes; commit any work in progress.
    3. Copy & paste the test. Change the names to describe the two behaviors.
    4. Change the test that calls the outer method to be independent of what happens in the inner method. One way to do this is to Extract Function on the unchanging part, then make the test verify that. You can leave a tiny outer part untested that just calls the two functions. There are other options as well, depending on the situation.
    5. Change the inner-behavior test to call your new function ([whatever]_beforeFeature[X]) instead of the outer function.
    6. Did you succeed at splitting the behaviors?
      yes:
      1. commit.
      no:
      1. revert.
      2. Add a new first line to the test that calls Feature.X.forTest().disable();
      3. Cut the test and paste it into both sub-describes.
      4. In the copy that is inside the after feature X sub-describe, change the first line to call Feature.X.forTest().enable(); instead.
  6. Done!

Introduce a place to later add the new behavior

  1. Use the executeCode method to conditionally execute code based on a feature flag, but do the same code in all cases.
    typescript
    function something() {
     const arbitrary = code();
     const i = Feature().Foo.executeCode({
     enabled: () => computeI_beforeFeatureFuzzyLogic(arbitrary),
     disabled: () => computeI_beforeFeatureFuzzyLogic(arbitrary),
     });
     return `nice ${i}`;
    }
    function computeI_beforeFeatureFuzzyLogic (arbitrary: unknown): int {
     let i = 2 + 2;
     if(arbitrary) i = i + 3;
     return i;
    }
  2. Duplicate the [whatever]_beforeFeature[X] function to make [whatever]_afterFeature[X], which is exactly the same.
  3. Update the executeCode call to use [whatever]_beforeFeature[X] on one branch and [whatever]_afterFeature[X] on the other.
  4. commit.
  5. Every test in the after feature X sub-describe calls [whatever]_beforeFeature[X] somewhere. Replace all those calls with [whatever]_afterFeature[X]. Search and replace within selection is good for this.
  6. commit.
  7. Done!

Test-drive the new behavior

  1. Alter a test in after feature X to make it fail.
  2. Change [whatever]_afterFeature[X] to make it pass.
  3. did any other tests fail?
    no:
    1. commit.
    another test in after feature X:
    1. revert the change and try again, or do it in smaller chunks. The usual TDD approach.
    a test in before feature X:
    1. Stop Now; failed

      This should never happen. Are you sure you changed the _afterFeatureX function?

    a test in the outer describe:
    1. Note: This means you missed a dependency between the inner and outer functions.
    2. Stash the changes
    3. Split the test as if you had identified it as "both" when you were spliting your tests.
    4. Apply the stash and your tests should be passing.
  4. Done!

Use Feature Flag to Change Rendering for a Component

  1. Use the showComponent method to conditionally render components based on a feature flag. For example, define two components oldComponent and newComponent using React Native components:
    typescript
    import React from "react";
    import { View, Text } from "react-native";
    
    const oldComponent = (
     <View>
     <Text>Old Component</Text>
     </View>
    );
    
    const newComponent = (
     <View>
     <Text>New Component</Text>
     </View>
    );
    
    // Render the appropriate component based on the feature flag:
    const renderedComponent = Feature().Foo.showComponent({
     enabled: newComponent,
     disabled: oldComponent,
    });
    
    // Then use renderedComponent in your component:
    const App = () => (
     <View>
     {renderedComponent}
     </View>
    );
    
    export default App;
  2. Create a test file to verify the executeCode usage.
    typescript
    import { Feature } from "../config/FeatureFlagInitiator";
    
    describe("executeCode usage", () => {
     it("should execute newCode when the flag is enabled", () => {
     // Force the flag to enabled for testing
     Feature().Foo.forTest.enable();
    
     let output = "";
     const oldCode = () => { output = "Old Code"; };
     const newCode = () => { output = "New Code"; };
    
     Feature().Foo.executeCode({
     enabled: newCode,
     disabled: oldCode,
     });
    
     expect(output).toBe("New Code");
     });
    
     it("should execute oldCode when the flag is disabled", () => {
     // Force the flag to disabled for testing
     Feature().Foo.forTest.disable();
    
     let output = "";
     const oldCode = () => { output = "Old Code"; };
     const newCode = () => { output = "New Code"; };
    
     Feature().Foo.executeCode({
     enabled: newCode,
     disabled: oldCode,
     });
    
     expect(output).toBe("Old Code");
     });
    });
  3. Create a separate test file to verify the showComponent usage.
    typescript
    import { Feature } from "../config/FeatureFlagInitiator";
    
    describe("showComponent usage", () => {
     it("should render newComponent when the flag is enabled", () => {
     // Force the flag to enabled for testing
     Feature().Foo.forTest.enable();
    
     const oldComponent = "Old Component";
     const newComponent = "New Component";
    
     const rendered = Feature().Foo.showComponent({
     enabled: newComponent,
     disabled: oldComponent,
     });
    
     expect(rendered).toBe("New Component");
     });
    
     it("should render oldComponent when the flag is disabled", () => {
     // Force the flag to disabled for testing
     Feature().Foo.forTest.disable();
    
     const oldComponent = "Old Component";
     const newComponent = "New Component";
    
     const rendered = Feature().Foo.showComponent({
     enabled: newComponent,
     disabled: oldComponent,
     });
    
     expect(rendered).toBe("Old Component");
     });
    });
  4. Done!