GithubHelp home page GithubHelp logo

tdeekens / flopflip Goto Github PK

View Code? Open in Web Editor NEW
400.0 5.0 40.0 41.85 MB

🎚Flip or flop features in your React application in real-time backed by flag provider of your choice 🚦

Home Page: https://techblog.commercetools.com/embracing-real-time-feature-toggling-in-your-react-application-a5e6052716a9

License: MIT License

JavaScript 56.68% TypeScript 43.13% Shell 0.19%
redux react feature-toggles feature-flags recompose javascript launchdarkly splitio lerna rollup

flopflip's Introduction

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 · Jest · TypeScript · @testing-library/react · Biome · Babel · Lodash · Changesets · Rollup 🙏

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



Want to see a demo? Logo

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

❯ Browser support

npx browserlist

and_chr 110
chrome 110
chrome 109
edge 110
edge 109
firefox 109
ios_saf 16.3
ios_saf 16.2
ios_saf 16.1
ios_saf 16.0
ios_saf 15.6
samsung 19.0

❯ 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
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 five packages in a memory-adapter, a localstorage-adapter or launchdarkly-adapter, react, react-redux and react-broadcast. You should not need the launchdarkly-adapter yourself but 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

❯ Demo

A minimal demo exists and can be adjusted to point to a custom LaunchDarkly account. You would have to create feature toggles according to the existing flags, though.

Then simply run:

  1. From the repositories root: yarn build:watch
  2. From /demo: first yarn and then yarn start

A browser window should open and the network tab should show feature flags being loaded from LaunchDarkly.

❯ Documentation

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.

@flopflip/react-redux & @flopflip/react-broadcast API

  • ConfigureFlopFlip a component to configure flopflip with an adapter (alternative to the store enhancer)
  • ReconfigureFlopFlip a component to reconfigure flopflip with new user properties either merged or overwriting old properties (shouldOverwrite prop)
    • useAdapterReconfiguration a hook to reconfigure flopflip with new user properties either merged or overwriting old properties (shouldOverwrite prop)
  • branchOnFeatureToggle a Higher-Order Component (HoC) to conditionally render components depending on feature toggle state
  • injectFeatureToggle a HoC to inject a feature toggle onto the props of a component
  • injectFeatureToggles a HoC to inject requested feature toggles from existing feature toggles onto the props of a component
  • ToggleFeature a component conditionally rendering its children based on the status of a passed feature flag <ToggleFeature> child based on the status of its passed feature flag
  • reducer and STATE_SLICE a reducer and the state slice for the feature toggle state
  • createFlopFlipEnhancer a redux store enhancer to configure flipflip and add feature toggle state to your redux store

Configuration

You can setup 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. More on how to use ConfigureFlopFlip below.

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

  1. Again using ConfigureFlopFlip for simpler use cases, or...
  2. or 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.

Setup using Components

Setup is easiest using ConfigureFlopFlip which is available in both @flopflip/react-broadcast and @flopflip/react-redux. Feel free to skip this section whenever setup using a store enhancer (in a redux context) is preferred.

It takes the props:

  • The adapter which can be e.g. launchdarkly-adapter
    • An adapter should implement the following methods: configure and reconfigure which both must return a Promise as configuration can be an asynchronous task
  • The adapterArgs containing whatever the underlying adapter accepts
    • The user object is often the basis to identify an user to toggle features. The user object can contain any additional data.
    • The adapter will receive onFlagsStateChange and onStatusStateChange will should be invoked accordingly to notify react-broadcast and react-redux about flag and status changes
  • The shouldDeferAdapterConfiguration prop can be used to defer the initial configuration the adapter. This might be helpful for cases in which you want to wait for e.g. the key to be present within your root component and you do not want flopflip to generate a uuid for you automatically.
  • The defaultFlags prop object can be used to specify default flag values until an adapter responds or in case flags were removed
  • The adapters expose function to update flags explicitely via adapter.updateFlags which eases updating flags and flushes them to all components via react-broadcast or react-redux

Different adapters allow for different configurations:

Please note that all adapters accept a user: TUser which has an optional key of type string. This user attribute can be used by each adapter to identify a user uniquely. Some adapters which require a user.key will generate a uuid whenever no key is passed.

1. The @flopflip/launchdarkly-adapter accepts

  • sdk.clientSideId: The client side id of LaunchDarkly
  • sdk.clientOptions: additional options to be passed to the underlying SDK
  • flags: defaulting to null to subscribe only to specific flags. Helpful when not wanting to subscribe to all flags to utilise LaunchDarkly's automatic flag archiving functionality
  • cacheMode: defaulting to null to change application of cached flags
    • The value can be eager to indicate that remote values should have effect immediately
    • The value can be lazy to indicate that values should be updated in the cache but only be applied once the adapter is configured again
  • throwOnInitializationFailure: defaulting to false to indicate if the adapter just re-throw an error during initialization
  • flagsUpdateDelayMs: defaulting to 0 to debounce the flag update subscription

2. The @flopflip/splitio-adapter accepts

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

3. The @flopflip/graphql-adapter accepts

  • uri: the uri to the GraphQL endpoint so e.g. https://graphql.com/graphql
  • query: the GraphQL query which returns features for instance query AllFeatures { flags: allFeatures { name \n value} }
  • getQueryVariables: a function called with adapterArgs being variables to your GraphQL query
  • getRequestHeaders: a function called with adapterArgs being headers to your GraphQL request
  • parseFlags: a function called with the data of fetched flags to parse the result before being exposed to your application. This function should be used to parse a query response into the TFlags type.
  • fetcher: a fetch implemtation if you prefer to not rely on the global fetch
  • pollingIntervalMs: the polling interval to check for updated flag values

4. The @flopflip/localstorage-adapter accepts

  • pollingIntervalMs: an interval at which the adapter polls for new flags from localstorage in milliseconds

5. The @flopflip/memory-adapter accepts

No special configuration is required for the memory adapter at this point.

Whenever you do not want to have the state of all flags persisted in redux the minimal configuration for a setup with @flopflip/react-broadcast and LaunchDarkly would be nothing more than:

import { ConfigureFlopFlip } from '@flopflip/react-redux';
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 }, user }}
>
  <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 }, user }}
>
  {(isAdapterConfigured) =>
    isAdapterConfigured ? <App /> : <LoadingSpinner />
  }
</ConfigureFlopFlip>
<ConfigureFlopFlip
  adapter={adapter}
  adapterArgs={{ sdk: { clientSideId }, user }}
  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>.

This variant of the ConfigureFlopFlip component form @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). If you're using @flopflip/react-broadcast you're done already.

Given your preference is to have the feature flag's state persisted in redux you would simply add a reducer when creating your store.

import { createStore, compose, applyMiddleware } from 'redux';
import {
  ConfigureFlopFlip,
  flopflipReducer,
  FLOPFLIP_STATE_SLICE
} from '@flopflip/react-redux';

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

const store = createStore(
  combineReducers({
    appReducer,
    [FLOPFLIP_STATE_SLICE]: flopflipReducer,
  }),
  initialState,
  compose(
    applyMiddleware(...),
  )
)

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 STATE_SLICE which is used internally too 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 { createStore, compose, applyMiddleware } from 'redux';
import {
  createFlopFlipEnhancer,
  flopflipReducer,

  // We refer to this state slice in the `injectFeatureToggles`
  // HoC and currently do not support a custom state slice.
  FLOPFLIP_STATE_SLICE
} from '@flopflip/react-redux';
import adapter from '@flopflip/launchdarkly-adapter';

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

const store = createStore(
  combineReducers({
    appReducer,
    [FLOPFLIP_STATE_SLICE]: flopflipReducer,
  }),
  initialState,
  compose(
    applyMiddleware(...),
    createFlopFlipEnhancer(
      adapter,
      {
        sdk: { clientSideId: window.application.env.LD_CLIENT_ID },
        user
      }
    )
  )
)

Note that @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.

Example:

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

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

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 applications's initialState as in the following anti-pattern example:

const initialState = {
  [FLOPFLIP_STATE_SLICE]: { flagA: true, flagB: false },
};
const store = createStore(
  // ...as before
  initialState
  // ...as before
);

Syncing the store reducer with adapters

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 setup 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 }, user }}
>
  <App />
</ConfigureFlopFlip>;

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.user) you can use ReconfigureFlopflip. ReconfigureFlopflip itself communicates with ConfigureFlopflip to reconfigure the given adapter for more fine grained targeting with the passed user. 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 }, user }}
>
  <>
    <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 user 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 user configuration will be overwritten not merged. Use with care as any subsequent shouldOverwrite={true} will overwrite any previously passed user with shouldOverwrite={false} (default).

@flopflip/react-broadcast & @flopflip/react-redux API

Apart from ConfigureFlopFlip both packages @flopflip/react-broadcast and @flopflip/react-redux export the same set of components to toggle based on features. Only the import changes depending on if you chose to integrate with redux or without. Again, behind the scenes the build on @flopflip/react to share common logic.

  • useFeatureToggle a React hook to read a single flag
  • useFeatureToggles a React hook to read multiple flags at once
  • useFlagVariation a React hook to read a single variation of a flag
  • useFlagVariations a React hook to read multiple variations of a flag at once
  • useAdapterStatus a React hook to read the underlying adapter's status
  • branchOnFeatureToggle a Higher-Order Component (HoC) to conditionally render components depending on feature toggle state
  • injectFeatureToggle a HoC to inject a feature toggle onto the props of a component
  • injectFeatureToggles a HoC to inject requested feature toggles from existing feature toggles onto the props of a component
  • ToggleFeature a component conditionally rendering its children based on the status of a passed feature flag

Note: that all passed flagNames passed as flag are a string. Depending on the adapter used these are normalized to be camel cased. This means that whenever a foo-flag-name is received in e.g. LaunchDarkly or splitio it will be converted to fooFlagName. The same applies for a foo_flag_name. This is meant to help using flags in an adapter agnostic way. Whenever a flag is passed in the non-normalized form it is also normalized again. Lastly, flopflip will show a warning message in the console in development mode whenever a non normalized flag name is passed.

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

Given you want to use React hooks within a functional component you can toggle as follows:

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[]

Given you want to use React hooks within a functional component you can toggle multiple flags at once as follows:

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

Given you want to use React hooks within a functional component you can read a variation as follows:

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>
   );
}

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

Given you want to use React hooks within a functional component you can read multiple variations as follows:

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

Given you want to use React hooks within a functional component you can read the adapter status as follows:

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

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

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

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 React, { Component } from 'react';
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>
);

or with for multi variate feature toggles:

const UntoggledComponent = () => <h3>{'At least there is a fallback!'}</h3>;

export default (
  <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>
);

or 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}
  />
);

or with Function as a Child (FaaC) which is always invoked with an isFeatureEnabled argument:

const UntoggledComponent = () => <h3>{'At least there is a fallback!'}</h3>;

export default (
  <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>
);

or with a render prop. Note that the render prop will only be invoked then the feature is turned on:

const UntoggledComponent = () => <h3>{'At least there is a fallback!'}</h3>;

export default (
  <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>}
  />
);

this last example will always turn the feature on if the variation or toggle does not exist. For this also look at defaultFlags for ConfigureFlopFlip.

We actually 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 crutial to your application.

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

or when the flag is multi variation

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.

Additional @flopflip/react-redux API

We also expose our internal selectors to access feature toggle(s) directly so that the use of injectFeatureToggle or injectFeatureToggles is not enforced or the only value to access flags from @flopflip/react-redux's store slice. The two selectors selectFeatureFlag and selectFeatureFlags return the same values for flags as injectFeatureToggle and injectFeatureToggles would.

An example usage for a connected component would be:

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 example above applies for selectFeatureFlags.

createFlopFlipEnhancer

Requires arguments of clientSideId:string, user:object.

  • The adapter
  • The adapterArgs object
    • Often with the before mentioned user object user object which often needs at least a key attribute

@flopflip/cypress-plugin

Introduction

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 seaching by E-Mail being enabled', () => {
    it('should allow adding users by E-Mail', () => {
      cy.updateFeatureFlags({ searchUsersByEmail: true });

      //... expectations
    });
  });
  describe('with seaching 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 @floplfip/cypress-plugin
npm install --save-dev @floplfip/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 or splitio. 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

@flopflip/react-redux and @flopflip/react-broadcast is built for UMD (un- and minified) and ESM using rollup.

Both our @flopflip/launchdarkly-wrapper and @flopflip/react packages are "only" build for ESM and CommonJS (not UMD) as they are meant to be consumed by a module loader to be integrated.

The package.json files contain a main and module entry to point to a CommonJS and ESM build.

  • ...ESM just import the dist/@flopflip/<package>.es.js within your app.
    • ...it's a transpiled version accessible via the pkg.module
  • ...CommonJS use the dist/@flopflip/<package>.cjsjs
  • ...AMD use the dist/@flopflip/<package>.umd.js
  • ...<script /> link it to dist/@flopflip/<package>.umd.js or dist/@flopflip/<package>.umd.min.js

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

Also feel free to use unpkg.com as a CDN to the dist files.

flopflip's People

Contributors

adnasa avatar alexhayton avatar atrakh avatar carloscortizasct avatar chrisw-b avatar danielruf avatar emmenko avatar fossabot avatar greenkeeper[bot] avatar ibratoev avatar is2ei avatar jaypea avatar jeffreyffs avatar jesse9009 avatar kerumen avatar marktran avatar montezume avatar ngbrown avatar omichelsen avatar p42-ai[bot] avatar renovate-bot avatar renovate[bot] avatar snyk-bot avatar tdeekens avatar vcapretz avatar yahiaeltai avatar zslabs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

flopflip's Issues

splitio-adapter: reconfigure does not work

Describe the bug
I was trying to configure flopflip with react-redux and splitio-adapter in the described way.

  1. use createFlopFlipEnhancer when creating the store. although there is no user-context available during store creation in my app.
  2. during app bootstrap or after login: call reconfigure on the adapter with the users key and details

the reconfigure method in https://github.com/tdeekens/flopflip/blob/master/packages/splitio-adapter/modules/adapter/adapter.ts#L192
does not change the user key and user config in the splitio client objects.

The splitio client itself does not provide an option to change the user key or user details besides configuring a new client.

this is what i do as a workaround, call configure instead of reconfigure in my redux actions after login. downside of this is, that i have to maintain some extra state to determine if the user has changed and i cannot use the createFlopFlipEnhancer

createFlopFlipEnhancer not working

Was trying to use createFlopFlipEnhancer in the app I'm working on and in the flopflip demo too.
The following error is showing up and am wondering why?

Unhandled promise rejection (rejection id: 79): Error: Actions must be plain objects. Use custom middleware for async actions.

isAdapterReady does not mean "flag is ready"

Describe the bug
👋 I've run into a bit of a roadblock where isAdapterReady (or using the useAdapterStatus hook) does not necessarily imply that useFeatureToggle is actually ready at the same time from LaunchDarkly. I'm using FlopFlip to directly impact whether a page is available to load - or if a user should be redirected to the homepage. This feels like a bit of a race-condition that I'm having some trouble getting around and wanted to see if you had any thoughts. Thanks!

To Reproduce
Steps to reproduce the behavior:

const featureEnabled = useFeatureToggle('feature')

  const { isReady } = useAdapterStatus()

  if (!isReady) {
    return <div>loading</div>
  } else if (!featureEnabled) {
    return <Redirect to="/" />
  }

  return <div>page stuff</div>

Expected behavior
Show loading at first, then show "page stuff" as feature flag is verified to be available.

`onlyUpdateForKeys` is not working as expected

I didn't understand well what onlyUpdateForKeys did and it seems is not solving the case I had. Even worse, it introduces a bug.. 😞

I see 2 issues: first, you call onlyUpdateForKeys on flagNames. But the component won't have a prop named flagNames. It will have a prop propKey. So I think the comparison have to be on this.

Second, my component can have other props. So if I update these props, it won't rerender. This is very problematic.

I'm investigating on my app how to solve these issues.

Stricter support for react 16.8 || 16.9

Is your feature request related to a problem? Please describe.
Using newer versions of react causes some warnings like

Warning: componentWillReceiveProps is deprecated and will be removed in the next major version. Use static getDerivedStateFromProps instead.
    
    Please update the following components: ConfigureAdapter
    
    Learn more about this warning here:
    https://fb.me/react-async-component-lifecycle-hooks

Describe the solution you'd like
Flopflip should probably drop support for react < 16.8 and rewrite the internal components to drop the deprecated lifecycle methods.

Describe alternatives you've considered
Not that I know of, maybe you have other solutions?

Additional context

commercetools/merchant-center-application-kit#801 (comment)

TypeScript definitions not accurate

Actual behavior:

dist/index/typings/index.d.ts does not export e.g. createFlipFlopEnhancer, but @flopflip-react-redux.es.js does.

So when importing in an IDE like e.g. WebStorm, the import shows as unresolved, because it checks the definitions file.

Desired behavior:
Type definitions include all exports exposed by es.js files, so they show as resolved imports in IDEs.

Usage in Typescript files causes TS3205

Things were working great with the library, then a teammate translated a file into TypeScript and this error started occurring.

account_page.tsx(8,10): error TS2305: Module ''@flopflip/react-broadcast'' has no exported member 'ToggleFeature'.

I figure this is an issue with not seeing the typings associated with FlopFlip or, worse, it can not understand the directory structure of @flopflip/react|react-broadcast in its resolution.

We have moduleResolution: 'node' in our tsconfig.json.

I've attempted a few things so far, such as changing typeRoots in tsconfig.json to additionally have the paths to the /types directories in react/react-broadcast respectively, but this has not worked.

Any ideas?

React-Redux: mistaken spread on state.flags

Describe the bug
When using the @flopflip/react-redux package after the last release, when a flag is updated, only the updated flag is present in Redux state, not the merged { prevState, newFlag }. In our case, this is in combination with the launchdarkly-adapter. It's possible this was introduced with the fix for #613

To Reproduce
Configure @flopflip with launchDarkly adapter and react-redux.

  1. Given turn a flag on or off in LaunchDarkly
  2. Then the redux flag state only contains the currently-changed flag; all the others have been removed.

Expected behavior
Should have the new flag + the previous flags.

Screenshots
image

Additional context
We have tracked this down to a recent change in flopflip/packages/react-redux/modules/ducks/flags/flags.ts:

The current code is in the reducer is:

switch (action.type) {
    case UPDATE_FLAGS:
      return {
        ...state.flags,
        ...action.payload.flags,
      };

    default:
      return state;
  }

However, it's the state itself that contains the flags; since state.flags is undefined, the object spread produces just the latest flag as the new state. The correct code should be:

case UPDATE_FLAGS:
  return { ...state, ...action.payload.flags };

Launch darkly adapter doesn't support reconfiguring with an empty/undefined user

I have my app wrapped with ConfigureFlopFlip from @flopflip/react-broadcast and configured with @flopflip/launchdarkly-adapter. When I log out and pass no user property in adapterArgs, the Launch darkly adapter breaks on this line:

if (adapterState.user.key !== user.key) {

Shouldn't this be something like the following to support the logout use-case?

  if (!user || adapterState.user.key !== user.key) {
    adapterState.user = ensureUser(user);

react-is dependency issue with react 16.9.0

Describe the bug
I'm not quite sure why the dependency is not being installed, but when attempting to upgrade a project to react 16.9.0, I'm getting the following:

This dependency was not found:
⠀
* react-is in ./node_modules/@flopflip/react-broadcast/dist/@flopflip-react-broadcast.umd.js

When installing this manually (version is 16.9.0 for this module) things seem to work as expected. Maybe we just need to update the dep within flopflip?

Fix Renovate config

Hi, I noticed some problems in your config and wanted to discuss them before submitting a Pull Request, as I'm not 100% sure I know how you'd like it. If you think the documentation is at fault, please feel free to point that out too.

First up, I want to alert you to renovatebot/renovate#1419
It's currently a design feature - but essentially a limitation - that config in a package.json applies only to that package.json. Early on it seemed a good way to provide different config to different package files in a monorepo, but I intend to change it. Because it's a breaking change, I was wanting to wait for feedback before merging it. In the meantime, you can move your config back to any of these files: renovate.json, .renovaterc, or .renovaterc.json so that the config applies to the entire monorepo.

If the config is placed inside a package.json once 1419 is merged then it actually needs to be inside a renovate object, not renovate-config. The latter is used for Renovate presets (that function like eslint shared configs) and not for cases like this. So in fact.. none of your config is activated right now - this repo is getting the app's default settings, which fortunately are sane.

Reviewing the content of your config:

      "extends": [
        "config:base"
      ],

This is good. Over time the bot's default settings are getting less "opinionated", as I pushed a few of those opinions into the config:base preset, which is then recommended when onboarding. For example with this preset it will automatically group dependencies that come from known monorepos like babel, react, etc.

      "packagePatterns": [
        "^@flopflip/"
      ],

This is technically invalid config, because packagePatterns needs to be inside a "packageRule", so it just ends up doing nothing. If you tell me what you were hoping to achieve, I can tell you how to do it.

      "lockFileMaintenance": {
        "enabled": true
      },

This is fine. It means every monday morning Renovate will regenerate your yarn.lock from scratch and you should see a "lock file maintenance" PR if there are any changes since the last time it was updated.

      "semanticCommits": false,

It looks like you are using semantic commits in this repo - did you definitely want to turn them off? You can configure both commit scope and type if desired.

      "prTitle": "{{semanticCommitType}}({{semanticCommitScope}}): {{#if isPin}}Pin{{else}}{{#if isRollback}}Roll back{{else}}Update{{/if}}{{/if}} {{depName}} to {{#unless isRange}}v{{/unless}}{{#if isMajor}}{{newVersionMajor}}.x{{else}}{{newVersion}}{{/if}}",
      "commitMessage": "{{semanticCommitType}}({{semanticCommitScope}}): Update {{depName}} to {{#unless isRange}}v{{/unless}}{{newVersion}}",

I'm not sure you need to configure/override these?

      "packageRules": [
        {
          "packagePatterns": ["*"],
          "excludePackagePatterns": [],
          "enabled": false
        }
      ]

This will in effect match every package name and therefore disable Renovate for all packages. Did you want to turn off all package updating except for your @flopflip ones or something like that? If so then the easiest thing is to add the ^@flopflip/ regex to excludePackagePatterns.

FYI I'll be back online tomorrow to follow up on any response.

[@flopflip/launchdarkly-adapter] Specify different key to trigger `reconfigure`

Hey there - I'm not sure if I have a unique use case or not, but here goes:

Issue:

  • We use a user's email as their key with LaunchDarkly
  • We have some flags that we configure on a per user basis / user segments. However, we also have flags that are dependent on the custom context of what organizationId and eventId the user is currently viewing.
  • This works great when forcing a new page load when the user jumps from event to event. However, when sticking with SPA routing, I can't figure out how to force the LaunchDarkly adapter to reconfigure because technically the user's key (email) is not changing.

Solution I'm thinking of:

  • My thinking is to allow a different key to be provided to the adapter for it to check against instead of using key. For example, if I pass a prop of reconfigureKey, the adapter will use that for it's internal validation. If reconfigureKey is not provided, it falls back to key.

Example solution:

  if (adapterState.user && (adapterState.user.configureKey !== user.configureKey || adapterState.user.key !== user.key)) {
    adapterState.user = ensureUser(user);

    return changeUserContext(adapterState.user);
  }

Reference line: https://github.com/tdeekens/flopflip/blob/master/packages/launchdarkly-adapter/modules/adapter/adapter.js#L75

I wanted to make sure I'm not blind to any other provided solutions. If so, please enlighten me! 🤓 Otherwise, let me know if you'd be interested in this as a PR - else, I can fork.

Appreciate your hard work!

Debounce of trottle adapter updating

Flopflip integrates with vairous adapters. Some of which maintain a websocket connection to the backing services. Anytime these servies change their state and send an event to the respective adapter the app will likely re-render in parts.

This behaviour is obviously intended and would arise with any sort of event based state updates from a remote server. However, an application gives away control over when it is re-rendered. As a result, whenever the flag provider runs into some "update-loop" of needless events being sent the UI will update recurrinly without any need.

As a result we could debounce or throttle the flag update callbacks within the adapters with a given, configured interval. This would also lead to the fact that flags updates are deferred to be only applied e.g. once every 30 seconds.

Thanks @ahmehri for reaching out.

launchdarkly-adapter is not creating the right user

There is an issue, I guess due to the recent refactoring.
In all the adapters the configure signature is:

const configure = ({
user,
onFlagsStateChange,
onStatusStateChange,
...remainingArgs
}) => {

Whereas in the launchdarkly-adapter you spread the user arg (since this commit 4998b88):

const configure = ({
clientSideId,
onFlagsStateChange,
onStatusStateChange,
...userArgs
}) => {

This doesn't respect the API as adapterArgs should take an user key, not a spreaded key.

So there is a change that have to be made. I can PR the change but I want to open the issue first for the discussion. Should I fix the adapter and re-accept a user key in the configure signature (not spreaded) or when the FlagsSubscription component call .configure(), spread the user key?

Let me know!

Refactor to remove `recompose` to add `react-powerplug`

The issue documents the intend to potentially refactor the library to remove recompose.

react-powerplug is a very nice library for declarative state management allowing a more natural composition than recompose.

Flopflop currently mostly uses recompose to wrap display names, inject props or render a component. Most of which could be achieved without using recompose at all. If state management would need to be abstracted into container components powerplug would shine.

Something for the next 🚋ride.

`ready` event is emitted on error when initializing client

Is your feature request related to a problem? Please describe.

A LaunchDarklyFlagFetchError is sometimes thrown during client initialization. This seems to occur because the ready event gets emitted as soon as the initialization logic has finished, regardless of whether it succeeded as explained here: launchdarkly/js-client-sdk#113 (comment)

Describe the solution you'd like

The ready event is only emitted on successful initialization

Describe alternatives you've considered

The waitUntilReady method call here

return adapterState.client.waitUntilReady().then(() => {

could be replaced with the waitForInitialization method as discussed here:
launchdarkly/js-client-sdk#113 (comment)

Change the flopflip reducer key

Currently, the flopflip reducer key is defined as @flopflip. It's a problem if you want to destructure your state (in a connect() for example) because you get a parsing error due to the @.

connect(({ @flopflip, user, .. }) => ({
  ...
}))

Maybe change the key to simply flopflip would be better.

Switch from create-react-context to React 16 context API?

I was investigating how the non-redux version works, and found you use create-react-context - should this be replaced with the new context API?

Also, for what it's worth, it was pretty confusing the non-redux package is called react-broadcast, for which there is already an npm package that has the same purpose as replacing the context api. I was looking for a while where react-broadcast was used, couldn't find it, and got more confused trying to figure out where context is coming from since there were no calls to createContext

Add option to not live update from feature flagging upstream

We have some mission critical workflows where we don't want the UI to live shift out from under our users. It would be nice to have the option to disable a live subscription to the flagging system.

I could probably hack around this with a HOC wrapper holding a <ConfigureFlopFlip /> that only passes the initial flag load down to components, but it would be nice to have this first class.

TS2306: File 'typings/index.d.ts' is not a module

I've cloned the current repo and ran the build. I'm linking my current project to the cloned flopflip repo. My webpack project is throwing the following error with tslint

ERROR in /projectDir/src/components/CopyTranscript/CopyTranscript.tsx
./src/components/CopyTranscript/CopyTranscript.tsx
[tsl] ERROR in /projectDir/src/components/CopyTranscript/CopyTranscript.tsx(1,31)
      TS2306: File '/projectDir/node_modules/flopflip/packages/react-redux/dist/typings/index.d.ts' is not a module.

[Idea]: Decouple from LaunchDarkly (add more adapters)

Currently flopflip is coupled to "only" LaunchDarkly as a provider for toggles this can change.

The library could potentially allow multiple providers for toggles and was actually built from the ground up to do so. Another packages/* to integrate with another provider and the configuration on of a consumer facing package should then pick any of the integrations behind the scenes. The API for an integration has a quite minimal surface area:

  1. Initialize (with allowing to defer it)
  2. Flush flags
  3. Flush updates

An integration is in charge of "doing" the targeting etc.

A simple idea is to integrate with an autoupdating/polling/streaming localstorage integration. Any app could, when receiving flags from it's backend, flush these to localstorage where the adapter/integration picks them up and they're automatically flushed to redux or the broadcasting variant.

Launch Darkly Goals

Describe the bug
When using Launch Darkly A/B tests, I am passing through the fetchGoals to clientOptions, but I'm not sure if my click targets are actually being populated back to LD; as there service also mentions adding the following to start tracking goals:

ldClient.track("your-goal-key", user);

If this is something FlopFlip doesn't do by default, would I have access to the client and user to be able to load that?

Expected behavior
Successful clicks should talk back to LD for the goal.

Rerender issues with `injectFeatureToggle`

I noticed an issue when my component has injectFeatureToggle. It injects the selected feature flags in the component as a prop. However, this is an object. So each time the "update" action is fired, the component is rerendered.
In my app, I use split.io and I needed to inject some flags at the root. It leads to perf issues because my whole tree is rerendered every 15 seconds (splitio fire an update event every 15 secs in dev, 30 in prod, even if the flags have not changed).

I think, one way to solve this would be, in the updateFlags from react-redux and react-broadcast, to deep check if the flags have changed, and if not, avoid dispatching the action. This will fix this globally (for every adapters).
Or we do this specifically in the splitio-adapter (because he's the guilty one) and we avoid calling onFlagsStateChange in the adapter.

For every cases, it would be better than dispatching an update action with an empty payload.

Add `withMultivariateToggle` HoC

Should add a new HoC to the react package which can be used by both react-broadcast and react-redux.

Currently multivariate toggles are supported by using injectFeatureToggles. This will pass the variate as a prop into the component. The component can then make rendering decisions based upon that. This could be eased a bit by introducing another HoC which might have the API withMultivariateToggle({<string>: <Component>}, propName:string):Component. This would inject the component as a propas specified with thepropName`.

withMultivariateToggle({
  'yellow': YellowButton,
  'red': RedButton,
  'default': Button
}, 'PrimaryButton')(Header)

this would inject a prop named PrimaryButton into the 'Header' component. Whenever none of the variations hold the default case would apply.

RFC: Flags Reducer for `ConfigureFlopflip`

This RFC proposes a feature to allow users to hook into and reduce state changes. This could be added to react-redux and react-broadcast.

How it would work

import { ConfigureFlopflip, FlopflipActions } from 'react-broadcast'

<ConfigureFlopflip 
  reducer={(prevState, nextState, action) => {
      if (action.type === FlopflipActions.UPDATE_FLAGS) {
        // do something e.g. evaluate if flopflip is initialised and
        // return default flags only for staff
         return nextState;
      }

      if (action.type === FlopflipActions.UPDATE_STATUS) {
         return { ...nextState, { flags: defaultFlags } };
      }
      
      return nextState;
   })
 >
   <App />
</ConfigureFlopflip>

This feature sits somewhere in between defaultFlags and targeting of flags powered by the provider. It might remove the need for defaultFlags entirely has they can be implemented in the reducer.

Use cases for the reducer prop could be:

  1. Removing the "need" for defaultFlags on ConfigureFlopflip
  2. Custom hooks into the flag "resolving" process e.g. before the adapter is initialised
  3. Logging "middleware" to log flag status changes (more valuable in a non react-redux environment)
  4. Persisting previous flags to localStorage and restoring them from there
    • Might solve the FOUC (Flash of unstyled content) when e.g. the FallbackComponent (of a branchOnFeatureFlag) renders for a split second before the adapter inititalised
    • Some adapters support this internally already

What this means for the library

Mostly that react-broadcast and react-redux would have an internalReducer which reduces state from an internal dispatch and passes that on to the reducer (which defaults to a identity function). In turn both libraries would work the same apart from react-redux handing over the result of the reducer to reduce via Redux again.

A nice side effect is that we could pass a dispatch to the render or FaaC of ConfigureFlopflip allowing consumers of the library to manipulate flags from their app without having to import the adapter.

<ConfigureFlopflip render={( { dispatch: dispatchFlopflip } ) => <App /> }>

Where further down in the tree somebody could

import { FlipFlipActions } from 'react-broadcast';

// some component stuff

dispatchFlopflip({ type: FlopflipActions.UPDATE_FLAGS, { myFeature: 'variationB' } })

This might contradict a bit the primary "source of truth" being the adapter but is an interesting thought experiment.

Note: Supporting a render-prop and FaaC on the ConfigureFlopflip might also be nice to pass down a changeUserContext whenever the adapter behind the scenes supports it.

Curious to what @dferber90 thinks. "That's silly" is a happily accepted opinion 🐱.

[RFC]: Add `ReconfigureFlopflip` component

There might be a case for a ReconfigureFlopflip component for this library.

Introduction

Flopflip is generally configured by ConfigureFloplip by passing an adapter, adapterArgs among other things. Depending on the underlying adapter the user context can change. This is handled automatically by the launchdarkly-adapter as of late. Whenever any of the user props change the underlying adapter will reconfigure through ConfigureFlopflip. This may yield a new set of flags depending on your flag configuration.

Case for the addition

A project or mine has a setup in which ConfigureFlopflip is rendered very high up the tree. Some of the user properties however as part of the adapterArgs are only known further down the component tree. This is instance can hold for any project with RR4 where a <Route> quite low in the tree. Imagine anything of that match prop should end up in the user of adapterArgs. You're stuck having to pass down an quite imperative setter function e.g. setProjectKey which somehow sideways changes the user context. If this is possible depends on the adapter. The ldAdapter exposes a changeUserContext which shallowly merges in any new passed props of a nextUser. This might seem doable at first but is not really declarative.

The same could be solved by a ReconfigureFlopflip which only receives e.g. two props nextAdapterArgs and a exact prop (defaulting to false). Depending on if exact is true the nextAdapterArgs are shallowly merged or overwritten.

Example

<Route
    exact={false}
    path="/:projectKey"
    render={routerProps => (
       <React.Fragment>
           <MyRouteComponent />
           <ReconfigureFlopflip exact={false} adapterArgs={{ projectKey: routerProps.projectKey }} />
       </React.Fragment>
    )}
/>

Changes required

  1. Adding the ReconfigureFlopflip component itself
  2. The ConfigureFlopflip has to pass the adapter (or only reconfigure) onto the context of flopflip
  3. Potentially use calculateChangeBits for the flags and reconfigure on the context to subscribe to only needed context state

Inconsistency between documentation to code

For example:
In readme.md appears:

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

instead variation must be variate according to code.

Also appears:

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

while the correct code is:

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

React-Redux: typescript types in selectFeatureFlag seem off

Describe the bug
ts(2345) Argument of type 'AppState' is not assignable to parameter of type 'Flags'.

To Reproduce
given i have a setup like this:

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

type AppState  = { 
  ...otherApplicationsStateSlices;
  [FLOPFLIP_STATE_SLICE]: Flags;
};

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

export default connect(mapStateToProps)(FooComponent);

Then i get the above mentioned typescript error.

Workaround is ugly and breaking type safety:
isFeatureOn: selectFeatureFlag('fooFlagName')((state as any) as Flags),

Expected behavior
Have another Type for the state in the definitions instead of :
export declare const selectFlag: (flagName: string) => (state: Flags) => string | boolean;
see https://github.com/tdeekens/flopflip/blob/master/packages/react-redux/modules/ducks/flags/flags.ts#L47

Is there another way to use this selector with typescript which i just didn't see?

Named exports in @flopflip/react-broadcast ?

Describe the bug
According to the Readme (as I read it), ConfigureFlopFlip and injectFeatureToggles are available through @flipflot/react-broadcast

Basically I would like to
import { ConfigureFlopFlip } from '@flopflip/react-broadcast'; and
import { injectFeatureToggles } from '@flopflip/react-broadcast';
but it does not work.

lerna ERR! yarn run build exited 1 in '@flopflip/launchdarkly-adapter'

This is on a fresh clone. I'm trying to just clone and build the current repo. Out of ideas.

flopflip$ npm cpx -v
6.4.1
flopflip$ npm install
npm WARN deprecated [email protected]: Package no longer supported. Contact [email protected] for more info.

> [email protected] install /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/fsevents
> node install

[fsevents] Success: "/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/fsevents/lib/binding/Release/node-v57-darwin-x64/fse.node" already installed
Pass --update-binary to reinstall or --build-from-source to recompile

> [email protected] install /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/iltorb
> detect-libc prebuild-install || node-gyp rebuild


> [email protected] install /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup-plugin-filesize/node_modules/iltorb
> node ./scripts/install.js || node-gyp rebuild

info looking for cached prebuild @ /Users/josh/.npm/_prebuilds/9415e2-iltorb-v2.4.0-node-v57-darwin-x64.tar.gz
info found cached prebuild 
info unpacking @ /Users/josh/.npm/_prebuilds/9415e2-iltorb-v2.4.0-node-v57-darwin-x64.tar.gz
info unpack resolved to /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup-plugin-filesize/node_modules/iltorb/build/bindings/iltorb.node
info unpack required /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup-plugin-filesize/node_modules/iltorb/build/bindings/iltorb.node successfully
info install Successfully installed iltorb binary!

> [email protected] install /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/husky
> node ./bin/install.js

husky
setting up Git hooks
trying to install from sub 'node_module' directory, skipping Git hooks installation

> flopflip@ postinstall /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip
> check-node-version --package --print && yarn build

node: 8.12.0
npm: 6.4.1
yarn: 1.6.0
yarn run v1.6.0
$ NODE_ENV=production lerna run build
lerna notice cli v3.3.2
lerna info versioning independent
lerna info Executing command in 7 packages: "yarn run build"
lerna ERR! yarn run build exited 1 in '@flopflip/launchdarkly-adapter'
lerna ERR! yarn run build stdout:
$ rimraf dist/**
$ cross-env npm run build:es && npm run build:cjs

> @flopflip/[email protected] build:es /Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/packages/launchdarkly-adapter
> cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.js -o dist/@flopflip-launchdarkly-adapter.es.js

info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

lerna ERR! yarn run build stderr:

modules/index.js → dist/@flopflip-launchdarkly-adapter.es.js...
(!) Unresolved dependencies
https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency
lodash.isequal (imported by modules/adapter/adapter.js)
lodash.camelcase (imported by modules/adapter/adapter.js)
warning (imported by modules/adapter/adapter.js)
@babel/runtime/helpers/defineProperty (imported by modules/adapter/adapter.js)
@babel/runtime/helpers/objectSpread (imported by modules/adapter/adapter.js)
@babel/runtime/helpers/slicedToArray (imported by modules/adapter/adapter.js)
@babel/runtime/helpers/typeof (imported by ../../../ldclient-js/dist/ldclient.es.js)
(!) Error when using sourcemap for reporting an error: Can't resolve original location of error.
modules/adapter/adapter.js: (6:9)
[!] Error: 'initialize' is not exported by ../../../ldclient-js/dist/ldclient.es.js
https://github.com/rollup/rollup/wiki/Troubleshooting#name-is-not-exported-by-module
modules/adapter/adapter.js (6:9)
4:   FlagVariation,
5:   User,
6:   Flag,
          ^
7:   Flags,
8:   OnFlagsStateChangeCallback,
Error: 'initialize' is not exported by ../../../ldclient-js/dist/ldclient.es.js
    at error (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:3438:30)
    at Module.error (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:13342:9)
    at handleMissingExport (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:13029:21)
    at Module.traceVariable (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:13450:17)
    at ModuleScope.findVariable (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:12709:29)
    at FunctionScope.Scope.findVariable (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:4000:68)
    at Scope.findVariable (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:4000:68)
    at Identifier$$1.bind (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:9649:40)
    at CallExpression.NodeBase.bind (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:9161:23)
    at CallExpression.bind (/Library/WebServer/Documents/g2m-interactive-recording/node_modules/flopflip/node_modules/rollup/dist/rollup.js:10219:31)

npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! @flopflip/[email protected] build:es: `cross-env NODE_ENV=development rollup -c ../../rollup.config.js -f es -i modules/index.js -o dist/@flopflip-launchdarkly-adapter.es.js`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the @flopflip/[email protected] build:es script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/josh/.npm/_logs/2018-09-17T19_36_25_661Z-debug.log
error Command failed with exit code 1.

lerna ERR! yarn run build exited 1 in '@flopflip/launchdarkly-adapter'
lerna WARN complete Waiting for 3 child processes to exit. CTRL-C to exit immediately.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! flopflip@ postinstall: `check-node-version --package --print && yarn build`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the flopflip@ postinstall script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/josh/.npm/_logs/2018-09-17T19_36_26_097Z-debug.log
flopflip$ 

UPDATE_FLAGS & UPDATE_STATUS action types are no longer exposed on ver. 5.1.0

UPDATE_FLAGS and UPDATE_STATUS action types used to be exposed on ver. 4.x.x and now they don't. I've used those two items to sync between several react applications while initialize launch darkly only once in one of those application but duplication actions values by putting it on a sort of publish/subscribers sort of solution. I would appreciate if you could expose it again instead of me creating a constant holding those values.

Allow reset of memory-adapter

First of all: Lovely package :)

I'm using the memory-adapter in an integration test to provide the default flags like this:

import memoryAdapter from '@flopflip/memory-adapter'

<ConfigureFlopFlip
  adapter={memoryAdapter}
  defaultFlags={{ someFeature: true }}
>
  <ComponentUnderTestWhichUsesFlags />
</ConfigureFlopFlip>

It would be great if it was possible to reset the memoryAdapter after each test like so

afterEach(() => {
  memoryAdapter.reset()
})

Do you think this is a good idea?

The closest thing to this I've found would be to do memoryAdapter.reconfigure(). Or should we have a dedicated test-adapter?

Loading state not exposed to app

It would be nice if a loading prop was available either as a function as child argument, so that consumers could opt in to not rendering their apps until the connection to launch darkly has succeeded (or failed)

`shouldDeferAdapterConfiguration` not working as intended

I have an issue with shouldDeferAdapterConfiguration in my app. I'll try to explain it: I have an XHR call which populate a user. So I set shouldDeferAdapterConfiguration={!user.key} and flopflip should not initialize until my user is ready. This is not the case. I investigated and maybe I found why.

First the component ConfigureFlopFlip mount and this condition match so the adapter is not configured. Good behaviour.

if (!this.props.shouldDeferAdapterConfiguration) {

Then, when my user is ready, componentWillReceiveProps is called with this.reconfigureOrQueue.

this.reconfigureOrQueue(nextProps.adapterArgs, { shouldOverwrite: true });

The adapter is not yet configured so setPendingAdapterArgs is called

this.adapterState === AdapterStates.CONFIGURED &&

My new user lives now in this.pendingAdapterArgs ready to be passed to the adapter but applyPendingAdapterArgs is never called

this.pendingAdapterArgs = mergeAdapterArgs(

componentDidUpdate occurs and now, this.props.shouldDeferAdapterConfiguration is false so configure is called, however this.state.appliedAdapterArgs doesn't have my user. So an error is triggered LaunchDarkly side.

I don't have enough visibility on the code yet to provide a fix. Let me know if any information can help :)

Improve the `getAll` flags to get real traffic data

On the 2 feature-flags services offered by the library (launchdarkly and split.io) they have the notion of an "used" flag. A flag which received traffic during the last few days. It's great because it helps remove dead code and flag which are not used anymore.
On flopflip, we require all the flags (because the library is designed like this) so we can't have the "real" traffic data. Even if a flag is unused we won't know it.

I think we should try to find a way to improve this.

Defaulting to camel cased flags.

I noticed multiple packages (splitio & LaunchDarkly) are converting incoming flags from their original form to be camel cased instead.

As a developer building out features using LaunchDarkly it seems to be easier to use the key supplied by the LaunchDarkly UI, copy & paste, instead of needing to camel case it before use.

I also did not see this documented anywhere. I lost a good amount of time with my flags returning false before digging into the library to see what was going on.


What's the philosophy surrounding the decision to camel case all incoming flags?

Maybe at the very least, we should add some documentation around this feature.

Add `injectFeatureToggle` HoC

Injecting a single feature toggle is currently "only" supported via the `withFeatureToggle' HoC.

Whenever a component should be toggled the withFeatureToggle HoC can be used. Whenever more sophisticated decisions need to be made upon multiple flags the injectFeatureFlags HoC can also be used.

However, in some cases not a whole component needs to be toggled as per withFeatureToggle. When injecting only one toggle injectFeatureToggles can become a bit verbose as you have to specify an array and in the component have to pluck the toggle off the featureToggles-prop again.

injectFeatureToggle('isButtonOn')(Header)

Would just inject isFeatureOn into the Header as a prop based on the state of isButtonOn.

An addition would be to specify the propName for the flag which may default to isFeatureOn so the API becomes

injectFeatureToggle('isButtonOn', 'shouldShowYellowButton')(Header)

Update flags

Describe the bug
I am unable to update flags using UPDATE_FLAGS constant from @flopflip/react-redux when using memory-adapter and giving initial state using createFlipFlopReducer.

You can view a demo of this error here: https://codesandbox.io/s/q8n8njyyow?fontsize=14, please open Page.js file.

To Reproduce
Steps to reproduce the behavior:

  1. Given
dispatch({
      type: UPDATE_FLAGS,
      payload: {
        flags: {
          socialLogin: {
            facebook: true,
            google: true
          }
        }
      }
    });
  1. Then state is:
{
  '@flopflip': {
    flags: {
      socialLogin: {
        facebook: false,
        google: false
      },
      flags: {
        socialLogin: {
          facebook: true,
          google: true
        }
      }
    },
    status: {
      status: {
        isReady: false
      }
    }
  }
}

The reason is that the state doesn't have a flag property:
image
But the reducer expects it.

It would work if the reducer is initiated this way:

export default createFlopflipReducer({ flags: features });

since FlipFlop state will now have a flags key in the flags object.

But it will break selectFeatureFlag and similar because they expect flags to be a direct child of reducer state.

Expected behavior
I expect it to replace or merge the flags object instead of appending it.

Screenshots
Demo at: https://codesandbox.io/s/q8n8njyyow?fontsize=14

Additional context

RFC: Improved feature variate toggling through `FeatureVariate`

The idea is to simply feature variante toggling through declarative compound components using FeatureToggled and FeatureVariante.

<FeatureToggled flag="signupForm">
   <FeatureVariate variate="version-1">
       <h1>Here we go new</h1>
   </FeatureVariate>
   <FeatureVariate variate="version-2">
       <h1>Here we go old</h1>
   </FeatureVariate>
</FeatureToggled>

Add ability to change the user context

LaunchDarkly's js-sdk allow switching the user context which could be ported to flopflip too.

The js-sdk has the API method identify to change a users' context. An example would be to have an anonymous user on a login page but an authed within the application. This would flush initial flags down to components and later new ones based on the authed user.

Currently flopflip supports the shouldInitialize callback prop to defer initialization with LaunchDarkly on e.g. a login page. Until then the static defaultFlags would be flushed to components. Changing the users' context would allow even more flexibility towards that.

Things to watch out for

  • Empty all flags when user context changes to not mix old with new
  • Keep client on component instance in FlagsSubscription to invoke identify
  • Just watching to changing key in componentWillReceiveProps should be enough

How do I set the initial flags when using memory-adapter?

I've got memory adapter working right now with Redux. However, it looks to me like the only way to add flags to the store is by using updateFlags({ fooFeature: true });.

Is there a way where I can preconfigure all default flags? I want to do an API call to retrieve new flags and update those manually whenever I feel like they need changing, but that doesn't mean I want to block components from rendering while the store is not populated yet.

Relevant code:

Redux store

import { createStore, applyMiddleware, compose } from 'redux';
import { createFlopFlipEnhancer } from '@flopflip/react-redux';
import adapter from '@flopflip/memory-adapter';
import rootReducer from './rootReducer';

const enhancer = compose(
  ...
  createFlopFlipEnhancer(adapter),
  ...
);

export default function configureStore(initialState = {}) {
  const store = createStore(rootReducer, initialState, enhancer);
  return store;
}

Reducer setup

import { combineReducers } from 'redux';
import { flopflipReducer, FLOPFLIP_STATE_SLICE } from '@flopflip/react-redux';
import utility from './reducers/utility';

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

export default rootReducer;

What I came up with on my own:

  1. Using createStore's preloadedState argument. This works, but it feels very wrong. This should be reserved for sending state from the server to client for state hydration. Not like a place to set default flags for my reducers.

  2. Not using flopflopReducer in my combineReducer method. I don't think this will work, and you're probably using this method for a good reason.

  3. Using updateFlags({}) on initial app load, before rendering the UI. This probably works, but it feels like a hack and like it would trigger new renders without any good reason.

Bindings for Reason 🤓

Since I'm in "Reason-mode" those days, would you be interested in providing some Reason bindings? 😄

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.