0.3.1 β€’ Published 4 years ago

@atg-digital/flags v0.3.1

Weekly downloads
-
License
ISC
Repository
github
Last release
4 years ago

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 and setFlag

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).

PropsTypeRequiredDescription
storeManagerStoreManager<T>trueStore 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 containing SF for San Francisco or NY for New York.
  • userAccounts is a flag that should be true 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.

PropsTypeRequiredDescription
childrenReactNodetrueReact 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.

PropsTypeRequiredDescription
nameKeyPath<T>trueMust be a valid key path of T
childrenReactNodefalseReact children
render(flags: T) => ReactNodefalseFunction that returns a ReactNode
fallbackRender(flags: T) => ReactNodefalseFunction that returns a ReactNode
componentComponentType<{ flags: T }>falseReact Component with T as props
fallbackComponentComponentType<{ flags: T }>falseReact 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 with T if defined or
    • call component with {flags: T} if defined else
    • return null
  • If the flag is falsy:
    • call fallbackRender with T if defined or
    • call fallbackComponent with { flags: T } if defined else
    • return null
<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.

ArgsTypeRequiredDescription
nameKeyPath<T>trueMust 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.

ArgsTypeRequiredDescription
nameKeyPath<T>falseMust be a valid key path of T
valueKeyPathValue<T, KP>falseMust 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:

ArgsTypeRequiredDescription
cookieNamestringtrueName of the cookie to store the flags
cookieOptionsCookieOptionstrueCookie options. For details, see universal-cookie options
defaultFlagsTtrueDefault flags to store in the cookie
validateFlagsValidateFlagsFunction<T>falseValidation 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
)