@atg-digital/flags v0.3.1
ATG Feature Flags
Feature flag library for React
npm install @atg-digital/flags
Usage
To use the library, you must initialize one instance of the API by calling
createFlag
function.
createFlags
The function createFlags()
is a factory method that creates:
- the React provider
FlagsProvider
- the React component
Flag
- the getters/setters
getFlag
,getFlags
andsetFlag
It is bound to a generic type variable T
which defines the type of the feature
flag object (in the example below, T
is equal to type Flags
).
Props | Type | Required | Description |
---|---|---|---|
storeManager | StoreManager<T> | true | Store manager used to persist the flags on client-side |
// flags.ts
import { createFlags, CookieStoreManager } from '@atg-digital/flags'
type Flags = {
api: {
version: {
v3: boolean
}
}
emailSubscription: boolean
enableAvatar: boolean
}
const flags: Flags = {
api: {
version: {
v3: false
}
},
emailSubscription: true,
enableAvatar: true
}
const flagsCookieName = 'atg_flag'
const flagsCookieOptions = { path: '/' }
const cookieStoreManager = new CookieStoreManager<Flags>(
flagsCookieName,
flagsCookieOptions,
flags
)
const { FlagsProvider, Flag, getFlag, getFlags, setFlag } = createFlags<Flags>(
cookieStoreManager
)
export { FlagsProvider, Flag, getFlag, getFlags, setFlag }
How-to set flags from browser's developer console
The library, by default, does not provide any way to set flags to the developer
console. To enable this functionality, the setFlag()
function (returned by
createFlags<T>()
) must be bound to the window
object:
window.ATGFlag = setFlag
In this way, any user from the developer console can call ATGFlag()
and change
flag's value.
API
In the following documentation, the type T
will refer to the type of the
feature flag provided to createFlag()
.
Computable
The library supports flags computed at runtime. A computed flag is defined as
the result of a function that takes as input all the flags and returns a value
compatible with that flag's type. A Computable
generic type is provided to
describe this kind of flag.
With computable flags, it is possible to support different environments at flag level. Letβs suppose:
ENV
is the environmental variable containingSF
for San Francisco orNY
for New York.userAccounts
is a flag that should betrue
for San Francisco only and only if API version 3 is used.
// flags.ts
import {
createFlags,
CookieStoreManager,
Computable
} from '@atg-digital/flags'
type Flags = {
api: {
version: {
v3: boolean
}
}
emailSubscription: boolean
userAccounts: boolean // Type of the computed flag
}
// π Computable type wrapping Flags
const flags: Computable<Flags> = {
api: {
version: {
v3: false
}
},
emailSubscription: true,
// π Function definition to compute flag's value at runtime
userAccounts: flags => ENV === 'SF' && flags.api.v3
}
FlagsProvider
Returned as part of createFlags()
. It is a React component that makes flags
available to children through the Context API. The flags are provided to this
component directly from the store manager; they should not be passed using the
props.
Props | Type | Required | Description |
---|---|---|---|
children | ReactNode | true | React children |
// index.tsx
import { MyApplication } from './app'
import { FlagsProvider } from './flags'
const instance = (
<FlagsProvider>
<MyApplication />
</FlagsProvider>
)
React.render(instance, document.querySelector('#app'))
Flag
Returned as part of createFlags()
. It renders a React component based on the
value of the flag (true
or false
). Must be used inside of FlagsProvider
.
Props | Type | Required | Description |
---|---|---|---|
name | KeyPath<T> | 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={['api', 'version', 'v3']}
fallbackRender={() => <div>Rendered if flag is false</div>}
>
<div>Rendered if flag is true</div>
</Flag>
getFlag
Returned as part of createFlags()
. It is a function that returns the value of
the flag taken in input. It is not a React hook so it can be used everywhere.
Args | Type | Required | Description |
---|---|---|---|
name | KeyPath<T> | true | Must be a valid key path of T |
// my-component.tsx
import { getFlag } from './flags'
const MyComponent = () => {
const enableAvatar = getFlag(['enableAvatar'])
return <div>Is avatar available? "{enableAvatar}"</div>
}
getFlags
Returned as part of createFlags()
. It is a function that returns a copy of all
the flags. It is not a React hook so it can be used everywhere.
// my-component.tsx
import { getFlags } from './flags'
const MyComponent = () => {
const flags = getFlags()
return <div>Is avatar available? "{flags.enableAvatar}"</div>
}
setFlag
Returned as part of createFlags()
. It is function used to change the value of
a flag. It returns an updated copy of all the flags. It is not a React hook so
it can be used everywhere.
Both of its arguments are optional; if they are not provided it behaves as
getFlags
and returns a copy of all the flags.
Args | Type | Required | Description |
---|---|---|---|
name | KeyPath<T> | false | Must be a valid key path of T |
value | KeyPathValue<T, KP> | false | Must be a valid value for a property of T |
import { setFlag } from './flags'
const updatedFlags = setFlag(['api', 'version', 'v3'], true)
Store Manager
The store manager is taken in input by createFlags()
to persist the flags in
the front-end.
It must implement the following interface:
interface StoreManager<T extends object> {
getFlags: () => T
getFlag: <KP extends KeyPath<T>>(name: KP) => KeyPathValue<T, KP>
setFlag: <KP extends KeyPath<T>>(name: KP, value: KeyPathValue<T, KP>) => T
}
And provides three methods:
getFlags
to retrieve all the flag stored.getFlag
to retrieve the value of the flag taken in input.setFlag
to save the new value for the flag take in input.
Cookie Store Manager
The library provides one implementation on the StoreManager
interface to save
the flags into a cookie: the class CookieStoreManager
.
This store manager does not save all the flag into the cookies but only the one
modified by calling setFlag
.
The constructor
takes in input the following arguments:
Args | Type | Required | Description |
---|---|---|---|
cookieName | string | true | Name of the cookie to store the flags |
cookieOptions | CookieOptions | true | Cookie options. For details, see universal-cookie options |
defaultFlags | T | true | Default flags to store in the cookie |
validateFlags | ValidateFlagsFunction<T> | false | Validation function used to validate the flag before saving it into the store. If the validation fails, the function must throw an error; otherwise it must return the flags taken in input. If not provided, no validation is done |
import { CookieStoreManager } from '@atg-digital/flags'
import { Flags, flagsSchema } from 'config'
const defaultflags: Flags = {
api: {
version: {
v3: false
}
},
emailSubscription: true,
enableAvatar: true
}
const flagsCookieName = 'atg_flag'
const flagsCookieOptions = { path: '/' }
const validateFlags = flags => {
const validatedFlags = flagsSchema.validateSync(flags, {
abortEarly: false,
stripUnknown: true
})
return validatedFlags
}
const cookieStoreManager = new CookieStoreManager<Flags>(
flagsCookieName,
flagsCookieOptions,
defaultflags,
validateFlags
)