Coder Social home page Coder Social logo

react-feature-toggles's People

Contributors

ericelliott avatar jnv avatar jwicks31 avatar kennylavender avatar nebual avatar nlambert avatar piotrsiatkowski avatar renovate-bot avatar renovate[bot] avatar thoragio 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  avatar  avatar  avatar  avatar  avatar

react-feature-toggles's Issues

Create Handler Issue

const createRoute = (features, { requiredFeature, ...methods }) => (req, res, next) => {
  const parsedUrl = parse(req.url, true);
  const { search } = parsedUrl;
  const updatedFeatures = updateFeatures(features, search);
  setStatus(res, getIsEnabled(updatedFeatures, requiredFeature));

  const handler = methods[req.method.toLowerCase()];
  if (handler !== undefined && typeof handler === 'function') {
    handler(req,res);
  }

  next();
};

next() being called after the handler is called creates an issue were the status gets overridden by a future handler

next() should not be called if the handler is called.

Smooth API rough edges

As long as we're thinking about a major version bump, let's examine the rest of the public API.

getEnabled() - deprecate

getEnabled() should be getEnabledFeatures() -- we can keep the old version and log deprecation warnings when it's used.

isFeatureIncluded() - rename & refocus

This is really the centerpiece of the low-level (non-component) public API.

isFeatureIncluded has an awkward name, and only makes sense in the context of a feature set that has already been decided. Also, it's advertised signature is wrong:

// This function is curried, not reflected in the signature.
isFeatureIncluded([...Strings], String) => Boolean

Maybe it should be hasFeature():

hasFeature = (featureName: String) => Boolean

Which can be a partial application of createHasFeature():

createHasFeature = (features: [...Feature]) => (hasFeature: String => Boolean)

Or in components, could delegate to a partially applied hasFeature() which can be dynamically swapped out if the features change at runtime.

getIsEnabled()

getIsEnabled() is confusing, and should probably not be part of the public API, even if we use it internally. End-users should be using hasFeature() instead, both on the server, and in the client.

Should we deprecate the publicly exposed version?

React components

In our React component props/context, do we need to expose anything other than a hasFeature() function that can be dynamically updated as feature statuses change?

Flatten the directory structure

Lots of nested folders and tests in different locations currently.

Something like this should be easier to update and manage.

Suggestions welcome.

This will be a breaking change to any project directly importing files

dist
source
  integration/
    |-- react-context.js
    |-- express.js
    |-- index.js
  test-fixtures/
    |-- createFeature.js
    |-- createFeatures.js
  test/
    |-- configure-features.js
    |-- create-route-middleware.js
    |-- get-enabled.js
    |-- get-is-enabled.js
    |-- is-feature-included.js
    |-- with-features.js
    |-- index.js
  |-- configure-features.js
  |-- create-route-middleware.js
  |-- get-enabled.js
  |-- get-is-enabled.js
  |-- is-feature-included.js
  |-- with-features.js
  |-- index.js
  • with-features
  • create-route-middleware
  • getEnabled
  • getIsEnabled
  • updateFeaturesWithParams
  • configure-feature
  • is-feature-included
  • move test fixtures
  • move integration tests

Missing test?

What if a feature is present but disabled? Is there a test for that case?

Auto build system for npm

I forgot nextjs controls its own babel setup and excludes the node modules folder so react feature toggles errors out. nextjs's babel setup can be overridden but its kind of annoying to have to do for a single package.

So setup a build system that auto builds before being published to npm.

rename getEnabledFeatures to getActiveFeatures

We are now using the terms active/inactive instead of enabled/disabled, rename getEnabledFeatures to getActiveFeatures so that it matches.

  • update README references
  • rename file
  • rename test file
  • rename named export and imports

Make exporting consistent

Fix any files still using default exports, all files should use only named exports.

src/index.js should export the destructured named exports directly, example:

export { funcName } from './file'
export { ComponentName } from './file'
export { funcName, ComponentName } from './file'

Use Node 8 only

From #18

We should make sure we're running Node 8 in our CI and prod environments and drop support for Node 6.

@thoragio you want to update this?

Implement getEnabledFeatures

part of #103

interface Feature {
  name: String,
  isActive: false,
  dependencies?: [...String]
}

getEnabledFeatures

([...Feature]) => [...String]

Takes an array of new Feature interface objects and returns an array of enabled feature names. This is nearly the same function as getEnabled, so the tests and functionality for this function can be copied.

Implement getCurrentActiveFeatures

in the spirit of automate all the things!

Name?

  • getAllActiveFeautres
  • getCurrentActiveFeatures

The goal of this function is to help reduce the code required to get an array of enabled feature names from initial Feature objects, the req.query, and browser query.

({ initialFeatures = [...String], req? , search? }) => [...String])

Writing more dreamcode will help!

Fix util importing

Importing is currently broken. We should create files in the root directory that refer to the appropriate files in the distribution. We hit that problem trying to use getIsEnabled. We had to import it like this:

import getIsEnabled from '@paralleldrive/react-feature-toggles/dist/utils/get-is-enabled';

revisit express middleware

createRouteMiddleware is currently not described in the documentation. Revisit the implementation and make sure it still makes sense for v2 and look for improvements that could be made.

Only exporting withFeatures restricts use cases.

WIP

Issue

Some projects using nextjs use loadGetInitialProps so that they can use getInitialProps in inner components. We currently only export withFeatures hoc. We can't add this functionality to withFeatures without making nextjs a dependency.

Applications can't make their own withFeatures hoc because we don't export a normal react component that they can build from.

Solution

Stuff will be here soon.

Dream Code

Some examples of the new code.

Creating a root component

import React from "react";
import { Provider } from "react-redux";
import { Features } from '@paralleldrive/react-feature-toggles';

const RootComponent = props => {
  const { store, history, initialFeatures,  query, children } = props;
  return (
    <Provider store={store}>
      <Features initialFeatures={initialFeatures} query={query}}>
        {children}
      </Features>
    </Provider>
  );
};

export default RootComponent;

Creating a HOC in some application that needs a custom hoc

import React, { Component } from 'react';
import { Features } from '@paralleldrive/react-feature-toggles';

const initialFeatures = [
  { name: 'foo', enabled: false },
  { name: 'bar', enabled: true },
];

const withFeatures = WrappedComponent => {
  class WithFeatures extends Component {
    static async getInitialProps(ctx) {
      const { req, query } = ctx;
      const subProps = await loadGetInitialProps(WrappedComponent, ctx);

      return {
        ...subProps,
        query
      };
    }
    render () {
      const query = this.props.query || this.context.query;
      return (
        <Features initialFeatures={initialFeatures} query={query}>
          <WrappedComponent {...this.props} />
        </Features>
      );
    }
  }
);

export default withFeatures;

New v2 proposal

@ericelliott, I started writing out dream code again with the goal of making the changes we discussed in issues #98 and #99 while also making sure it worked well with the new React context API.

Let me know what you think.

v2 Proposal

This is work in progress

This a proposal that attempts to simplify the API and name components and functions better.

I believe the provider component, Features, has more responsibility than needed. I am proposing we simplify the provider component to simply take an array of feature names. My reasoning is that the UI only cares about an array of active feature names. It does not care about things like feature dependencies. As you will see below, removing this logic from the provider component also allows for the rest of this libraries API to become simpler.

We will still provide functions that handle query parsing and feature dependencies but now they need to be used to calculate active features name array before it's been passed to the provider component.

Basic Usage

import { FAQComponent } from '../features/faq';
import { NotFoundComponent } from '../features/404-page';
import { FeatureToggles, Feature } from '@paralleldrive/react-feature-toggles';

const features = ['faq', 'foo', 'bar'];

const MyApp = () => {
  return (
    <FeatureToggles features={features}>
      <Feature name="faq" inactiveComponent={NotFoundComponent} activeComponent={FAQComponent}/>
    </FeatureToggles>
  )
}

API

Interfaces

Feature

interface Feature {
  name: String,
  isActive: false,
  dependencies?: [...String]
}

Functions

getEnabledFeatures

([...Feature]) => [...String]

Takes an array of feature objects and returns an array of enabled feature names.

parseQuery

(query = {}) => [...String]

Takes a query object and returns an array of enabled feature names.

const query = { ft='foo,bar,help' }
parseQuery(query); // ['foo', 'bar', 'help']

mergeFeatures

(...[...String]) => [...String]

Merge feature names without duplicating.

const currentFeatures = ['foo', 'bar', 'baz'];
mergeFeatures(currentFeatures, ['fish', 'bar', 'cat']); // ['foo', 'bar', 'baz', 'fish', 'cat']

deactivate

([...String], [...String]) => [...String]

Removes feature names

const currentFeatures = ['foo', 'bar', 'baz', 'cat'];
deactivate(currentFeatures, ['fish', 'bar', 'cat']); // ['foo', 'baz']

isActive

(featureName = "", features = [...String]) => boolean

Returns true if a feature name is in the array else it returns false.

const currentFeatures = ['foo', 'bar', 'baz'];
isActive('bar', currentFeatures); // true
isActive('cat', currentFeatures); // false

Components

FeatureToggles

FeatureToggles is a provider component.

props

  • features = []
import { FeatureToggles } from '@paralleldrive/react-feature-toggles';

const features = ['foo', 'bar', 'baz', 'cat'];

const MyApp = () => {
  return (
    <FeatureToggles features={features}>
      {.. stuff}
    </FeatureToggles>
  )
}

Feature

Feature is a consumer component.

If the feature is enabled then the activeComponent will render else it renders the inactiveComponent.

Feature takes these props

  • name = ""
  • inactiveComponent = noop
  • activeComponent = null

Feature will pass these props to both the inactiveComponent and the activeComponent

  • features = []
  • name = ""
import { FeatureToggles, Feature } from '@paralleldrive/react-feature-toggles';

const MyApp = () => {
  return (
    <FeatureToggles>
      <Feature name="faq" inactiveComponent={NotFoundComponent} activeComponent={FAQComponent}/>
      <Feature name="help" inactiveComponent={NotFoundComponent} activeComponent={HelpComponent}/>
    </FeatureToggles>
  )
}

Alternatively, you can use Feature as a render prop component by passing a function for the children.

import { FeatureToggles, Feature, isActive } from '@paralleldrive/react-feature-toggles';

const MyApp = () => {
  return (
    <FeatureToggles>
      <Feature>
        {({ features }) => isActive('bacon', features) ? 'The bacon feature is active' : 'Bacon is inactive' }
      </Feature>
    </FeatureToggles>
  )
}

HOCs ( Higher Order Components )

withFeatureToggles

({ features = [...String] } = {}) => Component => Component

You can use withFeatureToggles to compose your page functionality.

import MyPage from '../feautures/my-page';
import { withFeatureToggles } from '@paralleldrive/react-feature-toggles';

const features = ['foo', 'bar', 'baz', 'cat'];

export default = compose(
  withFeatureToggles({ features }),
  // ... other page HOC imports
  hoc1,
  hoc2,
);

Depending on your requirements, you might need something slightly different than the default withFeatureToggles. The default withFeatureToggles should serve as a good example to create your own.

configureFeature

(inactiveComponent, feature, activeComponent) => Component

configureFeature is a higher order component that allows you to configure a Feature component. configureFeature is auto curried so that you can partially apply the props.

import { FeatureToggles } from '@paralleldrive/react-feature-toggles';
const NotFoundPage = () => <div>404</div>;
const ChatPage = () => <div>Chat</div>;

const featureOr404 = configureFeature(NotFoundPage);
const Chat = featureOr404('chat', ChatPage);

const features = ['foo', 'bar', 'chat'];

const myPage = () => (
  <FeatureToggles features={features}>
    <Chat />
  </FeatureToggles>
);

Applying query overrides

Query logic has been moved out of the provider component, you should now handle this logic before passing features to FeatureToggles

import { FeatureToggles, mergeFeatures, parseQuery } from '@paralleldrive/react-feature-toggles';
import parse from 'url-parse';

const url = 'https://domain.com/foo?ft=foo,bar';
const query = parse(url, true);
const initialFeatures = ['faq', 'foo', 'bar'];
const features = mergeFeatures(initialFeatures, parseQuery(query));

const MyApp = () => {
  return (
    <FeatureToggles features={features}>
      {...stuff}
    </FeatureToggles>
  )
}

Implement getBrowserQueryFeatures

part of #103

Create a new function called getBrowserQueryFeatures.

(search?) => [...String]

  • it should be exported as part of the public API
  • it should be universal ( works in the browser and node )
  • given no arguments in node it should return an empty array
  • given no arguments in the browser it should use the global window.location.search to get the features
  • given a search string it should return the features
  • add the description to README

Examples:

// in the browser and the current url is: https://mydomain.com/help?ft=foo,bar
getBrowserQueryFeatures(); // ['foo', 'bar']

// or pass it a search string
getBrowserQueryFeatures('?ft=cat,bat'); // ['cat', 'bat']

rename parseQuery to getQueryFeatures

after implementing getBrowserQueryFeatures and getReqQueryFeatures it might make sense to rename parseQuery to getQueryFeatures

  • update README references
  • rename file
  • rename test file
  • rename named export and imports

Implement v2

Following the proposal outlined in #102

Implement update the following:

  • Update Readme
  • Components
    • FeatureToggles
    • Feature
  • HOCs
    • withFeatureToggles
    • configureFeature
  • Functions
    • isActive
    • mergeFeatures
    • deactivateFeatures
    • parseQuery
    • getEnabledFeatures
    • getBrowserQueryFeatures
    • getReqQueryFeatures
  • Update Feature Interface
  • Remove old code
    • components
    • hocs
    • functions
    • tests
  • Update createRouteMiddleware

After these are implemented, we need to build more automation of query parsing and merging of features.

Change the feature context in React

Currently with-features reduces the features it receives to an array of enabled feature names.

Preferable we will not reduce it down and keep all of the feature data.

This will allow us to use getEnabled and getIsEnabled on the React features context client side without having to create special functions.

This is a breaking change to projects that might have manually used the React features context.

will tackle after #70

Add rtype interface for initialFeatures to Readme.

For #16.

I am unsure how to describe initialFeatures in rtype, so I am experimenting here.

// example object
const initialFeatures = {
  'comments': {
    enabled: false,
    dependencies: []
  },
  'user-ratings': {
    enabled: false,
    dependencies: ['comments']
  }
}
interface Feature {
  enabled: Boolean,
  dependencies: Array
}
interface IntialFeatures {
  [featureName: String]: Feature
}

Implement parseQuery

part of #103

parseQuery

(query = {}) => [...String]

Takes a query object and returns an array of enabled feature names.

const query = { ft='foo,bar,help' }
parseQuery(query); // ['foo', 'bar', 'help']

Prep Project for NPM

Just a reminder at the moment, will create todo list here soon.

good starting point for myself: https://www.npmjs.com/package/npm-module-checklist

  • Add LICENSE
  • add license info to package.json
  • Add integration tests #15
  • setup eslint #9
  • Correct dependencies #8
  • Add snyk?
  • run linting and unit tests on each commit locally. pre-git, ghooks
  • add rtype signature(s) for initialFeatures #25
  • add supported node version to README?

rename deactivate to removeFeatures

because we have a function named mergeFeatures, I think removeFeatures makes more sense for this function name.

  • update README references
  • rename file
  • rename test file
  • rename named export and imports

Implement getReqQueryFeatures

part of #103

Create a new function called getReqQueryFeatures.

(req) => [...String]

A request object looks like this when it has features.

const req = {
  query = {
    ft: 'foo,bar,help'
  }
}

See req.query from express for more details on query objects.

  • it should be exported as part of the public API
  • given no arguments it should return an empty array
  • given anything but a req object with a query property and features it should return an empty array
  • given a request object, with a query property and features, it should return features
  • add the description to README

Clarifying real-life use-cases in Example

See #44

Maybe ComingSoonComponent should be called HelpFallbackComponent or something similar, to make it more clear.

In real use, I usually render entire pages as 404, or render nothing -- both of which users could configure as reusable fallback components that would work everywhere (like featureOr404(), featureOrNothing(), etc... I think the case where you render ComingSoon would be extremely rare. What you might render instead would be an older version of a component, like help404(HelpChatComponent, OldHelpComponent);

Confusing withFeatures example code

Is my brain broken, or does this example make no sense?

const Features = (withFeatures = { initialFeatures: [] }());

const Wrapper = () => <Features query={query} />;

Fix ramda imports

Ramda imports should import individual functions from the src folder to prevent tree shaking issues.

Fix any files that still import directly from the entry point.

Server side is ignoring ?ft= url param

The server side ignores the ?ft= url param, which causes the server to render a 404, but if the user waits a second, the client will take over and render the correct component with the feature enabled.

The server should see the ?ft= url param and send the rendered feature component to the client without a 404 response.

Switch all tests to riteway

Will tackle this after #70

unit tests

  • with-features
  • configure-feature
  • get-enabled
  • get-is-enabled
  • update-features-with-query

integration tests

  • context

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.