Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New v2 proposal #102

Closed
kennylavender opened this issue May 1, 2018 · 5 comments
Closed

New v2 proposal #102

kennylavender opened this issue May 1, 2018 · 5 comments
Assignees
Labels

Comments

@kennylavender
Copy link
Contributor

kennylavender commented May 1, 2018

@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>
  )
}
@kennylavender
Copy link
Contributor Author

@ericelliott @thoragio let me know your thoughts on this.

@ericelliott
Copy link
Contributor

ericelliott commented May 8, 2018

Perhaps we should call activate mergeFeatures instead? If I'm reading it right, a simple implementation might look like:

const mergeFeatures = (...featureLists) => featureLists.reduce((a, c) => [...new Set(a.concat(c))]);

I'm already envisioning a scenario for universal JS where I'd want to merge features from initialFeatures, req.query, and browser query string. Something like:

const browserQuery = ['elephant', 'fish', 'bar', 'cat'];
mergeFeatures(currentFeatures, parseNodeQuery(req && req.query), ['elephant', 'fish', 'bar', 'cat']);

Should Features be FeatureToggles in this example?

const NotFoundPage = () => <div>404</div>;
const ChatPage = () => <div>Chat</div>;

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

const myPage = () => (
  <Features>
    <Chat />
  </Features>
);

It's fine to move query parsing out of the React components - particularly for easy unit testing, however, the following bits of logic are going to be virtually identical in every app.

import parse from 'url-parse';

const url = 'https://domain.com/foo?ft=foo,bar'; // the user is going to have to get the query from the browser API here
const query = parse(url, true);
const initialFeatures = ['faq', 'foo', 'bar'];
const features = activate(initialFeatures, parseQuery(query));

I like the idea of providing both these low-level building blocks (e.g., parseQuery), and a higher-level plug-n-play version that actually pulls the and parses query parameter from the browser API, if its available:

import { getBrowserQueryFeatures } from '@paralleldrive/react-feature-toggles';

const activeFeatures = activate(initialFeatures, getBrowserQueryFeatures());

Ideally, that function would be universal, and return an empty array in Node. A similar function that takes req or undefined and returns an array of features extracted from req.query would also be useful. Then you could do:

const { getReqFeatures, getBrowserQueryFeatures } from '@paralleldrive/react-feature-toggles';

const browserFeatures = getBrowserQueryFeatures();
const reqFeatures = getReqFeatures(typeof req === 'object' ? req : undefined);

const activeFeatures = mergeFeatures(initialFeatures, reqFeatures, browserFeatures);

Sample implementation:

const getReqFeatures = ({ query = {}} = {}) => Object.keys(query);

Even this code is a bit too much boilerplate for my taste. This line is particularly obnoxious:

const reqFeatures = getReqFeatures(typeof req === 'object' ? req : undefined);

Maybe we could wrap that whole thing in a utility?

getActiveFeatures(initialFeatures: [...String]) => allTheFeatures: [...String]

Provide the primitives, but automate all the things! =)

@kennylavender
Copy link
Contributor Author

kennylavender commented May 9, 2018

@ericelliott

Perhaps we should call activate mergeFeatures instead? If I'm reading it right, a simple implementation might look like:

I debated about this, but after seeing your use case example, I like mergeFeatures that takes any amount of arrays better. I will update this.

Should Features be FeatureToggles in this example?

Yup, thanks! Fixed.

I like the direction your heading with the query utils and automation, I will mess around with those ideas build on top of them. Thanks!

@thoragio
Copy link
Contributor

thoragio commented May 9, 2018

@kennylavender This looks great! I like the new way of handling query logic, and the overall way the provider component is simplified. Really nice.

@ericelliott
Copy link
Contributor

Great work here. Let's make it happen!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants