Skip to content

tdeekens/flopflip

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2,650 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Logo

🎚 flopflip - Feature Toggling 🚦

Toggle (flip or flop) features being stored in Redux or in a broadcasting system (through the context) via a set of React components or HoCs.

❤️ React · Redux · Vitest · Turbo · TypeScript · @testing-library/react · Biome · Lodash · Changesets · tsup · pnpm 🙏

GitHub Action Status Codecov Coverage Status Known Vulnerabilities Made with Coffee

Embracing real-time feature toggling in your React application

Feature flagging with LaunchDarkly - Fun Fun Function

Details

Want to see a demo? Demo

Table of Contents

❯ Why you might use this

In summary feature toggling simplifies and speeds up your development processes. You can ship software more often, release to specified target audiences and test features with users (not only internal staff) before releasing them to everyone.

With flopflip you get many options and ways to toggle features. More elaborate examples below. For now imagine you have a new feature which is not finished developing. However, UX and QA already need access to it. It's hidden by a <Link> component redirecting. To toggle it all you need is:

<ToggleFeature flag="featureFlagName">
  <Link to="url/to/new/feature" />
</ToggleFeature>

Having flopflip setup up you can now target users by whatever you decide to send to e.g. LaunchDarkly. This could be location, hashed E-Mails or any user groups (please respect your user's privacy).

Another example would be to show a <button> but disable it for users who should not have access to the feature yet:

<ToggleFeature flag="featureFlagName">
  {({ isFeatureEnabled }) => (
    <button disabled={!isFeatureEnabled} onClick={this.handleClick}>
      Try out feature
    </button>
  )}
</ToggleFeature>

...or given you are using a React version with hooks and @flopflip/react-broadcast you can:

const MyFunctionComponent = () => {
  const isFeatureEnabled = useFeatureToggle('featureFlagName');
  const handleClick = () => console.log('🦄');

  return (
    <button disabled={!isFeatureEnabled} onClick={handleClick}>
      Try out feature
    </button>
  );
};

In all examples flags will update in realtime (depending on the adapter and provider) and the User Interface will update accordingly. If this sounds interesting to you, keep reading.

❯ Package Status

Package Version Downloads Sizes
react react Version react Version react Downloads react Minified + GZipped
react-broadcast react-broadcast Version react-broadcast Version react-broadcast Downloads react-broadcast Minified + GZipped
react-redux react-redux Version react-redux Version react-redux Downloads react-redux Minified + GZipped
launchdarkly-adapter launchdarkly-adapter Version launchdarkly-adapter Version launchdarkly-adapter Downloads launchdarkly-adapter Minified + GZipped
splitio-adapter splitio-adapter Version splitio-adapter Version splitio-adapter Downloads splitio-adapter Minified + GZipped
memory-adapter memory-adapter Version memory-adapter Version memory-adapter Downloads memory-adapter Minified + GZipped
localstorage-adapter localstorage-adapter Version localstorage-adapter Version localstorage-adapter Downloads localstorage-adapter Minified + GZipped
graphql-adapter graphql-adapter Version graphql-adapter Version graphql-adapter Downloads graphql-adapter Minified + GZipped
http-adapter http-adapter Version http-adapter Version http-adapter Downloads http-adapter Minified + GZipped
combine-adapters combine-adapters Version combine-adapters Version combine-adapters Downloads combine-adapters Minified + GZipped
cypress-plugin cypress-plugin Version cypress-plugin Version cypress-plugin Downloads cypress-plugin Minified + GZipped
types types Version types Version types Downloads types Minified + GZipped

❯ Installation

This is a mono repository maintained using changesets. It currently contains multiple packages including adapters (launchdarkly-adapter, splitio-adapter, graphql-adapter, http-adapter, memory-adapter, localstorage-adapter), integration bindings (react-broadcast, react-redux), a shared react package, a cypress-plugin, combine-adapters, and supporting utilities (types, cache, adapter-utilities). You should not need an adapter package directly but rather one of our bindings (react-broadcast or react-redux). Both use the react package to share components.

Depending on the preferred integration (with or without redux) use:

yarn add @flopflip/react-redux or npm i @flopflip/react-redux --save

or

yarn add @flopflip/react-broadcast or npm i @flopflip/react-broadcast --save

❯ Getting Started

Flopflip allows you to manage feature flags through the notion of adapters (e.g. LaunchDarkly or LocalStorage) within an application written using React with or without Redux.

You can set up flopflip to work in two ways:

  1. Use React's Context (hidden for you) via @flopflip/react-broadcast
  2. Integrate with Redux via @flopflip/react-redux

Often using @flopflip/react-broadcast will be the easiest way to get started. You would just need to pick an adapter which can be any of the provided. Either just a memory-adapter or an integration with LaunchDarkly via launchdarkly-adapter will work.

Whenever you want the flag state to live in Redux you can use @flopflip/react-redux which can be set up in two variations itself:

  1. Using ConfigureFlopFlip for simpler use cases
  2. With a Redux store enhancer

The store enhancer replaces ConfigureFlopFlip for setup and gives the ability to pass in a preloadedState as default flags. For ConfigureFlopFlip the default flags would be passed as a defaultFlags prop.

Using react-broadcast (Context)

The minimal configuration for a setup with @flopflip/react-broadcast and LaunchDarkly would be nothing more than:

import { ConfigureFlopFlip } from '@flopflip/react-broadcast';
import { adapter } from '@flopflip/launchdarkly-adapter';
// or import { adapter } from '@flopflip/memory-adapter';
// or import { adapter } from '@flopflip/localstorage-adapter';

<ConfigureFlopFlip
  adapter={adapter}
  adapterArgs={{ sdk: { clientSideId }, context }}
>
  <App />
</ConfigureFlopFlip>;

You can also pass render or children as a function to act differently based on the underlying adapter's ready state:

<ConfigureFlopFlip
  adapter={adapter}
  adapterArgs={{ sdk: { clientSideId }, context }}
>
  {(isAdapterConfigured) =>
    isAdapterConfigured ? <App /> : <LoadingSpinner />
  }
</ConfigureFlopFlip>
<ConfigureFlopFlip
  adapter={adapter}
  adapterArgs={{ sdk: { clientSideId }, context }}
  render={() => <App />}
/>

Note that children will be called with a loading state prop while render will only be called when the adapter is configured. This behaviour mirrors the workings of <ToggleFeature>.

ConfigureFlopFlip from @flopflip/react-broadcast will use the context and a broadcasting system to reliably communicate with children toggling features (you do not have to worry about any component returning false from shouldComponentUpdate).

Using react-redux (Redux)

If you prefer to have the feature flag's state persisted in redux you would add a reducer when creating your store.

import { configureStore, combineReducers } from '@reduxjs/toolkit';
import {
  ConfigureFlopFlip,
  flopflipReducer,
  FLOPFLIP_STATE_SLICE,
} from '@flopflip/react-redux';

// Maintained somewhere within your application
import { user } from './user';
import { appReducer } from './reducer';

const rootReducer = combineReducers({
  appReducer,
  [FLOPFLIP_STATE_SLICE]: flopflipReducer,
});

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
  preloadedState: initialState,
});

export default store;

Setup through a Redux store enhancer

Another way to configure flopflip is using a store enhancer. For this a flopflip reducer should be wired up with a combineReducers within your application in coordination with the FLOPFLIP_STATE_SLICE which is used internally to manage the location of the feature toggle states. This setup eliminates the need to use ConfigureFlopFlip somewhere else in your application's component tree.

In context this configuration could look like:

import { configureStore, combineReducers } from '@reduxjs/toolkit';
import {
  createFlopFlipEnhancer,
  flopflipReducer,
  FLOPFLIP_STATE_SLICE,
} from '@flopflip/react-redux';
import { adapter } from '@flopflip/launchdarkly-adapter';

// Maintained somewhere within your application
import { context } from './context';
import { appReducer } from './reducer';

const rootReducer = combineReducers({
  appReducer,
  [FLOPFLIP_STATE_SLICE]: flopflipReducer,
});

const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(
      createFlopFlipEnhancer(adapter, {
        sdk: { clientSideId: window.application.env.LD_CLIENT_ID },
        context,
      }),
    ),
  preloadedState: initialState,
});

export default store;

@flopflip/react-redux also exports a createFlopflipReducer(preloadedState: Flags). This is useful when you want to populate the redux store with initial values for your flags:

const defaultFlags = { flagA: true, flagB: false };

const rootReducer = combineReducers({
  appReducer,
  [FLOPFLIP_STATE_SLICE]: createFlopflipReducer(defaultFlags),
});

const store = configureStore({
  reducer: rootReducer,
});

This way you can pass defaultFlags as the preloadedState directly into the flopflipReducer. This means you do not need to keep track of it in your application's initialState as in the following anti-pattern example:

Avoid this pattern -- use createFlopflipReducer instead.

const initialState = {
  [FLOPFLIP_STATE_SLICE]: { flagA: true, flagB: false },
};

const rootReducer = combineReducers({
  appReducer,
  [FLOPFLIP_STATE_SLICE]: flopflipReducer,
});

const store = configureStore({
  reducer: rootReducer,
  preloadedState: initialState,
});

In addition to initiating flopflip when creating your store, you could still wrap most or all of your application's tree in ConfigureFlopFlip. This is needed when you want to identify as a user and set up the integration with LaunchDarkly or any other flag provider or adapter.

Note: This is not needed when using the memory-adapter.

import { adapter } from '@flopflip/launchdarkly-adapter';

<ConfigureFlopFlip
  adapter={adapter}
  adapterArgs={{ sdk: { clientSideId }, context }}
>
  <App />
</ConfigureFlopFlip>;

Reconfiguring adapters

Whenever your application "gains" certain information (e.g. with react-router) only further down the tree but that information should be used for user targeting (through adapterArgs.context for LaunchDarkly or adapterArgs.user for other adapters) you can use ReconfigureFlopFlip. ReconfigureFlopFlip itself communicates with ConfigureFlopFlip to reconfigure the given adapter for more fine grained targeting with the passed context. You also do not have to worry about rendering any number of ReconfigureFlopFlips before the adapter is initialized (e.g. LaunchDarkly). Requested reconfigurations will be queued and processed once the adapter is configured.

Imagine having ConfigureFlopFlip above a given component wrapped by a Route:

<ConfigureFlopFlip
  adapter={adapter}
  adapterArgs={{ sdk: { clientSideId }, context }}
>
  <>
    <SomeOtherAppComponent />
    <Route
      exact={false}
      path="/:projectKey"
      render={(routerProps) => (
        <>
          <MyRouteComponent />
          <ReconfigureFlopFlip
            // Note: This is the default - feel free to omit unless you want to set it to `true`.
            shouldOverwrite={false}
            // Note: this should be memoised to not trigger wasteful `reconfiguration`s.
            user={{ projectKey: routerProps.projectKey }}
          />
        </>
      )}
    />
  </>
</ConfigureFlopFlip>

Internally, ReconfigureFlopFlip will pass the projectKey to ConfigureFlopFlip, causing the adapter to automatically update the context and therefore to flush new flags from the adapter (given they are provided by e.g. LaunchDarkly).

Note: Whenever shouldOverwrite is true the existing configuration will be overwritten not merged. Use with care as any subsequent shouldOverwrite={true} will overwrite any previously passed properties with shouldOverwrite={false} (default).

❯ Adapters

All adapters are configured through the adapterArgs prop of ConfigureFlopFlip. Most adapters accept a user: TUser with an optional key of type string to identify a user uniquely.

The ConfigureFlopFlip component accepts the following props:

Prop Description
adapter The adapter to use (e.g. launchdarkly-adapter). An adapter should implement the configure and reconfigure methods, which both must return a Promise as configuration can be asynchronous.
adapterArgs Whatever the underlying adapter accepts. The adapter will receive onFlagsStateChange and onStatusStateChange which should be invoked accordingly to notify react-broadcast and react-redux about flag and status changes.
shouldDeferAdapterConfiguration Defer initial adapter configuration (useful when waiting for a key to be present).
defaultFlags Default flag values until an adapter responds or in case flags were removed.

Adapters expose adapter.updateFlags to update flags explicitly, flushing them to all components via react-broadcast or react-redux.

LaunchDarkly adapter

Note: The LaunchDarkly adapter uses context: LDContext instead of user.

Prop Default Description
context The LaunchDarkly context (LDContext) used to identify the user.
sdk.clientSideId The client side id of LaunchDarkly.
sdk.clientOptions Additional options to be passed to the underlying SDK.
flags null Subscribe only to specific flags. Helpful when not wanting to subscribe to all flags to utilise LaunchDarkly's automatic flag archiving functionality.
cacheMode null Change application of cached flags. eager: remote values should have effect immediately. lazy: values should be updated in the cache but only be applied once the adapter is configured again.
throwOnInitializationFailure false Indicate if the adapter should re-throw an error during initialization.
flagsUpdateDelayMs 0 Debounce the flag update subscription.
initializationTimeout 2 (seconds) Set the timeout for waitForInitialization.

Split.io adapter

Prop Description
sdk.authorizationKey Authorization key for splitio.
sdk.options General attributes passed to splitio SDK.
sdk.treatmentAttributes The treatment attributes passed to splitio.

GraphQL adapter

Prop Default Description
uri The URI to the GraphQL endpoint (e.g. https://graphql.com/graphql).
query The GraphQL query which returns features (e.g. query AllFeatures { flags: allFeatures { name \n value} }).
getQueryVariables A function called with adapterArgs returning variables for your GraphQL query.
getRequestHeaders A function called with adapterArgs returning headers for your GraphQL request.
parseFlags A function called with the data of fetched flags to parse the result into the TFlags type.
fetcher A fetch implementation if you prefer to not rely on the global fetch.
pollingIntervalMs 60000 The polling interval in milliseconds to check for updated flag values.

HTTP adapter

Prop Default Description
execute A function called with adapterArgs which must return a Promise resolving to flags.
pollingIntervalMs 60000 The polling interval in milliseconds.

LocalStorage adapter

Prop Default Description
pollingIntervalMs 60000 The interval in milliseconds at which the adapter polls for new flags from localstorage.

Memory adapter

No special configuration is required for the memory adapter.

❯ API Reference

Both @flopflip/react-broadcast and @flopflip/react-redux export the same set of hooks, components, and HoCs. Only the import changes depending on if you chose to integrate with redux or without. Behind the scenes they build on @flopflip/react to share common logic.

Flag normalization: All flag names are strings. Depending on the adapter used, these are normalized to camelCase. A foo-flag-name received from e.g. LaunchDarkly or splitio will be converted to fooFlagName. The same applies for foo_flag_name. This is meant to help using flags in an adapter-agnostic way. Whenever a flag is passed in non-normalized form it is also normalized again. flopflip will show a warning message in the console in development mode whenever a non-normalized flag name is passed.

Hooks

useFeatureToggle(flagName: string, flagVariation: FlagVariation): boolean

Read a single flag's toggle state:

import { useFeatureToggle } from "@flopflip/react-broadcast";

const ComponentWithFeatureToggle = (props) => {
  const isFeatureEnabled = useFeatureToggle("myFeatureToggle");

  return (
    <h3>{props.title}</h3>
    <p>The feature is {isFeatureEnabled ? "enabled" : "disabled"}</p>
  );
};

useFeatureToggles({ [flagName: string]: FlagVariation }): boolean[]

Read multiple flags at once:

import { useFeatureToggles } from "@flopflip/react-broadcast";

const ComponentWithFeatureToggles = (props) => {
  const [isFirstFeatureEnabled, isV2SignUpEnabled] = useFeatureToggles({
    myFeatureToggle: true,
    mySignUpVariation: "signUpV2",
  });

  return (
    <h3>{props.title}</h3>
    <p>The first feature is {isFirstFeatureEnabled ? "enabled" : "disabled"}</p>
    <p>The v2 signup feature is {isV2SignUpEnabled ? "enabled" : "disabled"}</p>
  );
};

useFlagVariation(flagName: string): FlagVariation

Read a single flag's variation value:

import { useFlagVariation } from "@flopflip/react-broadcast";

const ComponentWithFeatureToggle = (props) => {
  const featureVariation = useFlagVariation("myFeatureToggle");

  return (
    <h3>{props.title}</h3>
    <p>The feature variation is {featureVariation}</p>
  );
};

useFlagVariations([flagName: string]): FlagVariation[]

Read multiple flag variation values at once:

import { useFlagVariations } from "@flopflip/react-broadcast";

const ComponentWithFeatureToggle = (props) => {
  const [featureVariation1, featureVariation2] = useFlagVariations([
    "myFeatureToggle1",
    "myFeatureToggle2",
  ]);

  return (
    <h3>{props.title}</h3>
    <ul>
      <li>The feature variation 1 is {featureVariation1}</li>
      <li>The feature variation 2 is {featureVariation2}</li>
    </ul>
  );
};

useAdapterStatus(): AdapterStatus

Read the underlying adapter's status:

import { useAdapterStatus } from '@flopflip/react-broadcast';

const ComponentWithFeatureToggle = () => {
  const isFeatureEnabled = useFeatureToggle('myFeatureToggle');
  const { isConfigured } = useAdapterStatus();

  if (!isConfigured) return <LoadingSpinner />;
  else if (!isFeatureEnabled) return <PageNotFound />;
  else return <FeatureComponent />;
};

useAdapterReconfiguration()

Reconfigure the adapter with new properties (either merged or overwriting old properties).

useAllFeatureToggles()

Read all feature toggles at once.

Components

ConfigureFlopFlip

See the Getting Started section for usage.

ReconfigureFlopFlip

See Reconfiguring adapters for usage.

ToggleFeature

The component renders its children depending on the state of a given feature flag. It also allows passing an optional untoggledComponent which will be rendered whenever the feature is disabled instead of null.

import { ToggleFeature } from '@flopflip/react-redux';
// or import { ToggleFeature } from '@flopflip/react-broadcast';
import { flagsNames } from './feature-flags';

const UntoggledComponent = () => <h3>{'At least there is a fallback!'}</h3>;
export default (
  <ToggleFeature
    flag={flagsNames.THE_FEATURE_TOGGLE}
    untoggledComponent={UntoggledComponent}
  >
    <h3>I might be gone or there!</h3>
  </ToggleFeature>
);

For multi-variate feature toggles, pass a variation prop:

<ToggleFeature
  flag={flagsNames.THE_FEATURE_TOGGLE.NAME}
  variation={flagsNames.THE_FEATURE_TOGGLE.VARIATES.A}
  untoggledComponent={UntoggledComponent}
>
  <h3>I might be gone or there!</h3>
</ToggleFeature>

ToggleFeature supports several rendering patterns:

Pattern Description
children (element) Rendered when the feature is enabled
children (function) Called with { isFeatureEnabled }, always invoked
render (function) Called only when the feature is enabled
toggledComponent Component rendered when the feature is enabled
untoggledComponent Component rendered when the feature is disabled

Example with toggledComponent prop:

const UntoggledComponent = () => <h3>{'At least there is a fallback!'}</h3>;
const ToggledComponent = () => <h3>{'I might be gone or there!'}</h3>;

export default (
  <ToggleFeature
    flag={flagsNames.THE_FEATURE_TOGGLE.NAME}
    variation={flagsNames.THE_FEATURE_TOGGLE.VARIATES.A}
    untoggledComponent={UntoggledComponent}
    toggledComponent={ToggledComponent}
  />
);

Example with Function as a Child (FaaC):

<ToggleFeature
  flag={flagsNames.THE_FEATURE_TOGGLE.NAME}
  variation={flagsNames.THE_FEATURE_TOGGLE.VARIATES.A}
  untoggledComponent={UntoggledComponent}
>
  {({ isFeatureEnabled }) => <h3>I might be gone or there!</h3>}
</ToggleFeature>

Example with render prop (only invoked when the feature is on):

<ToggleFeature
  flag={flagsNames.THE_FEATURE_TOGGLE.NAME}
  variation={flagsNames.THE_FEATURE_TOGGLE.VARIATES.A}
  untoggledComponent={UntoggledComponent}
  render={() => <h3>I might be gone or there!</h3>}
/>

We recommend maintaining a list of constants with feature flag names somewhere within your application. This avoids typos and unexpected behavior. After all, the correct workings of your feature flags is crucial to your application.

TestProviderFlopFlip

Exported from @flopflip/react-broadcast only. A test provider component for testing feature flag behaviour in your tests.

Higher-Order Components

branchOnFeatureToggle({ flag: String, variation?: String | Boolean })

A HoC to conditionally render a component based on a feature toggle's state. It accepts the feature toggle name and an optional component to be rendered in case the feature is disabled.

Without a component rendered in place of the ComponentToBeToggled:

import { branchOnFeatureToggle } from '@flopflip/react-redux';
import flagsNames from './feature-flags';

const ComponentToBeToggled = () => <h3>I might be gone or there!</h3>;

export default branchOnFeatureToggle({ flag: flagsNames.THE_FEATURE_TOGGLE })(
  ComponentToBeToggled,
);

With a component rendered in place of the ComponentToBeToggled:

import { branchOnFeatureToggle } from '@flopflip/react-redux';
import { flagsNames } from './feature-flags';

const ComponentToBeToggled = () => <h3>I might be gone or there!</h3>;
const ComponentToBeRenderedInstead = () => (
  <h3>At least there is a fallback!</h3>
);

export default branchOnFeatureToggle(
  { flag: flagsNames.THE_FEATURE_TOGGLE },
  ComponentToBeRenderedInstead,
)(ComponentToBeToggled);

With a multi-variate flag:

import { branchOnFeatureToggle } from '@flopflip/react-redux';
import { flagsNames } from './feature-flags';

const ComponentToBeToggled = () => <h3>I might be gone or there!</h3>;
const ComponentToBeRenderedInstead = () => (
  <h3>At least there is a fallback!</h3>
);

export default branchOnFeatureToggle(
  {
    flag: flagsNames.THE_FEATURE_TOGGLE,
    variation: 'variate1',
  },
  ComponentToBeRenderedInstead,
)(ComponentToBeToggled);

injectFeatureToggles(flagNames: Array<String>, propKey?: String, areOwnPropsEqual?: Function)

This HoC matches feature toggles given against configured ones and injects the matching result.

import { injectFeatureToggles } from '@flopflip/react-redux';
import { flagsNames } from './feature-flags';

const Component = (props) => {
  if (props.featureToggles[flagsNames.TOGGLE_A])
    return <h3>Something to render!</h3>;
  else if (props.featureToggles[flagsNames.TOGGLE_B])
    return <h3>Something else to render!</h3>;

  return <h3>Something different to render!</h3>;
};

export default injectFeatureToggles([flagsNames.TOGGLE_A, flagsNames.TOGGLE_B])(
  Component,
);

injectFeatureToggle(flag: String, propKey?: String)

This HoC matches feature toggles given against configured ones and injects the matching result. branchOnFeatureToggle uses this to conditionally render a component. You also may pass a second argument to overwrite the default propKey of the injected toggle (defaults to isFeatureEnabled).

import { injectFeatureToggle } from '@flopflip/react-redux';
import { flagsNames } from './feature-flags';

const Component = (props) => {
  if (props.isFeatureEnabled) return <h3>Something to render!</h3>;

  return <h3>Something different to render!</h3>;
};

export default injectFeatureToggle(flagsNames.TOGGLE_B)(Component);

The feature flags will be available as props within the component allowing some custom decisions based on their value.

Redux API (react-redux only)

flopflipReducer / createFlopflipReducer

The reducer for the feature toggle state. See Using react-redux and Setup through a Redux store enhancer for usage.

FLOPFLIP_STATE_SLICE

The state slice key used for the feature toggle state in the Redux store.

createFlopFlipEnhancer

Requires two arguments:

  • The adapter (e.g. imported from @flopflip/launchdarkly-adapter)
  • The adapterArgs object containing the adapter-specific configuration (e.g. { sdk: { clientSideId }, context } for LaunchDarkly)

selectFeatureFlag / selectFeatureFlags

Selectors to access feature toggle(s) directly from the Redux store, so that the use of injectFeatureToggle or injectFeatureToggles is not enforced. They return the same values for flags as injectFeatureToggle and injectFeatureToggles would.

import { selectFeatureFlag } from '@flopflip/react-redux';

const mapStateToProps = (state) => ({
  someOtherState: state.someOtherState,
  isFeatureOn: selectFeatureFlag('fooFlagName')(state),
});

export default connect(mapStateToProps)(FooComponent);

As an alternative to using injectFeatureToggle:

const mapStateToProps = (state) => ({
  someOtherState: state.someOtherState,
});

export default compose(
  injectFeatureToggle('fooFlagName'),
  connect(mapStateToProps),
)(FooComponent);

The same pattern applies for selectFeatureFlags.

❯ Cypress Plugin

Changing flag state in End-to-End test runs helps ensuring that an application works as expected with all variations of a feature. For this @flopflip comes with a cypress-plugin. This plugin tightly integrates with any underlying adapter and allows altering flag state from within test runs.

Imagine having the following Cypress test suite:

describe('adding users', () => {
  describe('with searching by E-Mail being enabled', () => {
    it('should allow adding users by E-Mail', () => {
      cy.updateFeatureFlags({ searchUsersByEmail: true });

      //... expectations
    });
  });
  describe('with searching by E-Mail being disabled', () => {
    it('should allow adding users by name', () => {
      cy.updateFeatureFlags({ searchUsersByEmail: false });

      //... expectations
    });
  });
});

In the example above we test two variations of a feature. Being able to alter flag state during test runs avoids work-arounds such as complex multi-project setups and makes the tests themselves resilient to changes of your flag configurations on your staging or testing environments.

Installation

To install the @flopflip/cypress-plugin you will have to add the respective command and plugin as follows after installing it as a devDependency.

yarn add --dev @flopflip/cypress-plugin
npm install --save-dev @flopflip/cypress-plugin

In the plugins/index.js add the following to your existing config:

+const flopflipCypressPlugin = require('@flopflip/cypress-plugin');

module.exports = (on, cypressConfig) => {

+flopflipCypressPlugin.install(on);

  return { };
};

In the support/index.js add the following to your existing commands:

import { addCommands as addFlopflipCommands } from '@flopflip/cypress-plugin';

addFlopflipCommands({
  adapterId: 'launchdarkly',
});

Please note that the adapterId should be one of launchdarkly, memory, localstorage, splitio, graphql or http. It allows the cypress-plugin to hook into the respective adapter. Also make sure to update to the most recent version of any adapter to ensure a smooth integration between the plugin and the adapter.

❯ Module Formats

All packages are built for ESM and CommonJS using tsup (powered by esbuild).

The package.json files contain a main entry pointing to ./dist/index.js and an exports map with import and require conditions:

  • ESM: ./dist/index.js
  • CommonJS: ./dist/index.cjs

All build files are part of the npm distribution using the files array to keep install time short.

❯ Browser Support

Configured via .browserslistrc:

[production]
supports es6-module and >0.25%
not ie 11
not op_mini all

[ssr]
node 12

Run npx browserslist to see the resolved browser list for the current environment.