Appearance
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
- Complete Initial Setup for Feature Flags before proceeding.
- Unless you are re-using an existing flag, add your new feature flag to the
@/platform/features.ts
file. Add it to theprod_flags
asFlag.Disabled
, and todev_flags
(or a similar env of your choice) asFlags.Enabled
. - 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:
- Accomplish the goal Use Feature Flag to Alter Code Behavior
- rendering:
- Accomplish the goal Use Feature Flag to Change Rendering for a Component
- Done!
Use Feature Flag to Alter Code Behavior
- Accomplish the goal Isolate the code that is changing
- Accomplish the goal Split feature-invariant from feature-dependent behaviors in tests
- Accomplish the goal Introduce a place to later add the new behavior
- Accomplish the goal Test-drive the new behavior
- Done!
Isolate the code that is changing
- 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}`; }
- 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.typescriptfunction 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; }
- Done!
Split feature-invariant from feature-dependent behaviors in tests
- 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]
andafter feature [X]
.typescriptdescribe("original thing", () => { it("first test", () => {}); // ... describe("before feature FuzzyLogic", () => { }); describe("after feature FuzzyLogic", () => { }); });
- Add a
beforeEach
andafterEach
to the outer describe.typescriptbeforeEach(() => { Feature.X.forTest().enable(); }); afterEach(() => { Feature.X.forTest().reset(); });
- commit.
- Go through each unit test for the outer method (
something
in our example). For each test: - Does that test verify logic / depend on the stuff inside the method you extracted, outside it, or both?
- inside:
- Change the test to call your new function (
[whatever]_beforeFeature[X]
) instead of the outer function. - Make sure it passes, then commit.
- Cut this test and paste it into both new sub-describes.
- commit.
- Change the test to call your new function (
- outside:
- 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:
- 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.
- make sure you have no local changes; commit any work in progress.
- Copy & paste the test. Change the names to describe the two behaviors.
- 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. - Change the inner-behavior test to call your new function (
[whatever]_beforeFeature[X]
) instead of the outer function. - Did you succeed at splitting the behaviors?
- yes:
- commit.
- no:
- revert.
- Add a new first line to the test that calls
Feature.X.forTest().disable();
- Cut the test and paste it into both sub-describes.
- In the copy that is inside the
after feature X
sub-describe, change the first line to callFeature.X.forTest().enable();
instead.
- Done!
Introduce a place to later add the new behavior
- Use the
executeCode
method to conditionally execute code based on a feature flag, but do the same code in all cases.typescriptfunction 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; }
- Duplicate the
[whatever]_beforeFeature[X]
function to make[whatever]_afterFeature[X]
, which is exactly the same. - Update the
executeCode
call to use[whatever]_beforeFeature[X]
on one branch and[whatever]_afterFeature[X]
on the other. - commit.
- 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. - commit.
- Done!
Test-drive the new behavior
- Alter a test in
after feature X
to make it fail. - Change
[whatever]_afterFeature[X]
to make it pass. - did any other tests fail?
- no:
- commit.
- another test in after feature X:
- revert the change and try again, or do it in smaller chunks. The usual TDD approach.
- a test in before feature X:
Stop Now; failed
This should never happen. Are you sure you changed the
_afterFeatureX
function?
- a test in the outer describe:
- Note: This means you missed a dependency between the inner and outer functions.
- Stash the changes
- Split the test as if you had identified it as "both" when you were spliting your tests.
- Apply the stash and your tests should be passing.
- Done!
Use Feature Flag to Change Rendering for a Component
- Use the
showComponent
method to conditionally render components based on a feature flag. For example, define two componentsoldComponent
andnewComponent
using React Native components:typescriptimport 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;
- Create a test file to verify the
executeCode
usage.typescriptimport { 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"); }); });
- Create a separate test file to verify the
showComponent
usage.typescriptimport { 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"); }); });
- Done!