Feature flagging made easy for React and Redux
yarn add flag
Feature flagging is necessary for large client-side applications. They improve development speed and allow teams to test new features before they are stable. In order to WANT to use feature flags in an application, they should be VERY easy to add and remove. That means minimal boiler plate and no need to pass boolean props down through component hierarchy. Such a thing could be done with global variables; however, they live outside of the React/Redux lifecycle, making them more difficult to control. Instead, this library injects and then accesses feature flags directly from the React context without getting in your way.
Flag allows you to declare flags as either plain values or as functions. If a flag is a function then it is referred to as a computed flag. The function accepts one argument which is the flags object itself. You do not have to use computed flags, but they can be very convenient. For example,
const flags = {
// properties can be nested objects
features: {
// they can be boolean
useMyCoolNewThing: true
},
config: {
// they can be strings
apiUrl: "www.example.com/api"
},
// they can be numbers
cool: 1,
dude: 5,
// they can be computed
coolAndDude: flags => flags.cool + flags.dude,
// they can be computed from other computed properties.
// other computed properties are resolved for you, so that you do not
// need to call it as a function.
largeCoolAndDude: flags => flags.coolAndDude > 10
};
This library has strong TypeScript support as of v4. In order to get that support, you must
initialize the flag
library before using it.
Creates React bindings for flags. You should only initialize one instance of this API. Does
not take any value arguments, but takes one type argument T
which is the shape of your resolved
flags.
// flags.ts
import createFlags from "flag";
export type MyFlags = {
features: {
useMyCoolNewThing: boolean;
};
config: {
apiUrl: string;
};
cool: number;
dude: number;
coolAndDude: number;
largeCoolAndDude: boolean;
};
const { FlagsProvider, Flag, useFlag, useFlags } = createFlags<MyFlags>();
export { FlagsProvider, Flag, useFlag, useFlags };
You can also add support for Redux by importing createReduxBindings
from flag/redux
.
Args | Type | Required | Description |
---|---|---|---|
provider |
FlagsProvider<T> |
true |
Provider created by createFlags |
// flags.ts
// ... the above
import createReduxBindings from "flag/redux";
const {
setFlagsAction,
getFlagsSelector,
createFlagsReducer,
ConnectedFlagsProvider
} = createReduxBindings(FlagsProvider);
export {
setFlagsAction,
getFlagsSelector,
createFlagsReducer,
ConnectedFlagsProvider
};
For brevity, the type T
in the section below refers to the shape of your resolved feature flags.
Generic type used to describe unresolved flags. Very useful when including functions are part of your flag definitions because function arguments can be inferred.
import { Computable } from "flag";
type MyFlags = {
a: boolean;
b: boolean;
c: boolean;
};
const flags: Computable<MyFlags> = {
a: true,
b: false,
// π `flags` type checks!
c: flags => flags.a && flags.b
};
Returned as part of createFlags()
. React component that makes flags available to children through the Context API.
Props | Type | Required | Description |
---|---|---|---|
flags |
Computable<T> |
true |
All pre-computed flags |
children |
ReactNode |
true |
React children |
// index.tsx
import { MyApplication } from "./app";
import { FlagsProvider, Flag } from "./flags";
const instance = (
<FlagsProvider flags={flags}>
<MyApplication />
</FlagsProvider>
);
React.render(instance, document.querySelector("#app"));
Returned as part of createFlags()
. Renders a some UI based on whether a flag is truthy or falsy. It's a glorified if statement π¬. Must be used in side of FlagsProvider
.
Props | Type | Required | Description |
---|---|---|---|
name |
string[] |
true |
Must be a valid key path of T |
children |
ReactNode |
false |
React children |
render |
(flags: T) => ReactNode |
false |
Function that returns a ReactNode |
fallbackRender |
(flags: T) => ReactNode |
false |
Function that returns a ReactNode |
component |
ComponentType<{ flags: T }> |
false |
React Component with T as props |
fallbackComponent |
ComponentType<{ flags: T }> |
false |
React Component with T as props |
Order of deciding which of these nodes to renders is as follows:
- If the flag is
truthy
:- render
children
if defined - call
render
withT
if defined or - call
component
with{flags: T}
if defined else - return
null
- render
- If the flag is
falsy
:- call
fallbackRender
withT
if defined or - call
fallbackComponent
with{ flags: T }
if defined else - return
null
- call
<Flag
name={["features", "useMyCoolNewThing"]}
render={() => <div>Rendered on truthy</div>}
fallbackRender={() => <div>Rendered on falsy</div>}
/>
Returned as part of createFlags()
. A React hook that returns all of the flags. Must be used in side of FlagsProvider
.
// my-component.tsx
import { useFlags } from "./flags";
const MyComponent = () => {
const flags = useFlags();
return <div>The API url is "{flags.config.apiUrl}"</div>;
};
Returned as part of createFlags()
. A React hook to return a single flag. Must be used in side of FlagsProvider
.
Args | Type | Required | Description |
---|---|---|---|
keyPath |
string[] |
true |
Must be a valid key path of T |
// my-component.tsx
import { useFlags } from "./flags";
const MyComponent = () => {
const apiUrl = useFlag(["config", "apiUrl"]);
return <div>The API url is "{apiUrl}"</div>;
};
Returned as part of createReduxBindings(...)
. Creates a reducer to be used in your Redux stores.
Args | Type | Required | Description |
---|---|---|---|
flags |
T |
true |
The initial value of your flags |
// reducer.ts
import { combineReducers } from "redux";
import { Computable } from "flag";
import { createFlagsReducer, MyFlags } from "./flags";
import { otherReducer } from "./other-reducer";
const flags: Computable<MyFlags> = {
// ...
};
export default combineReducers({
// π must use the "flags" key of your state
flags: createFlagsReducer(flags),
other: otherReducer
});
A selector to retrieve computed flags from Redux state. It is not enough to say state.flags
because createFlagsReducer
does not eagerly evaluate computable flags.
(Though I suppose if you don't use any computable flags, then you don't necessarily need this π€·ββοΈ.)
// reducer.ts
import { getFlagsSelector, MyFlags } from "./flags";
type State = {
flags: MyFlags;
// ...
};
// ...
export const getFlags = (state: State) => getFlagsSelector(state);
Returned as part of createReduxBindings(...)
. Wraps FlagsProvider
, fetching the flags from Redux state.
import { Provider } from "redux";
import { MyApplication } from "./app";
import { ConnectedFlagsProvider } from "./flags";
import { store } from "./store";
const instance = (
<Provider store={store}>
<ConnectedFlagsProvider>
<MyApplication />
</ConnectedFlagsProvider>
</Provider>
);
React.render(instance, document.querySelector("#app"));
Returned as part of createReduxBindings(...)
. A dispatchable action that sets flags. Merges a partial value of pre-computed flags with existing pre-computed flags.
Args | Type | Required | Description |
---|---|---|---|
flags |
Computable<T> |
true |
Partial pre-computed flags |
import { Thunk } from "redux-thunk";
import { setFlagsAction } from "./flags";
export const someThunk: Thunk<any> = ({ dispatch }) => async () => {
const user = await fetchUser();
dispatch(setFlagsAction(user.flags));
// ...
};
MPL-2.0