paralleldrive / react-feature-toggles Goto Github PK
View Code? Open in Web Editor NEWFeature Toggles for React Projects
License: MIT License
Feature Toggles for React Projects
License: MIT License
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.
Add note to read me about v2 breaking changes and see the 1.x tag for the current docs
When using param overrides in the browser, react server/client rendering will be different so react displays a console error. Add a notice about this to the README.
As long as we're thinking about a major version bump, let's examine the rest of the public API.
getEnabled()
should be getEnabledFeatures()
-- we can keep the old version and log deprecation warnings when it's used.
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()
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?
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?
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
part of #103
What if a feature is present but disabled? Is there a test for that case?
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.
We are now using the terms active/inactive
instead of enabled/disabled
, rename getEnabledFeatures to getActiveFeatures so that it matches.
See rtype.
If you want to enable type hints in your editor, use parameter default values and make sure you're using an editor that supports type inference (see Tern.js or VS Code's built-in type inference).
Add functionality for serverside 404 statuses and server side 404 rendering.
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'
The client doesn't send the entire features object in order to save load time. Let's provide a util to easily check.
part of #103
interface Feature {
name: String,
isActive: false,
dependencies?: [...String]
}
([...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.
in the spirit of automate all the things!
Name?
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!
Tape's glob imports are now fixed in version 4.8
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';
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.
https://docs.npmjs.com/private-modules/ci-server-config
To use a private npm package it looks like we will need to get an auth token for the parallel npm account and set the auth token in each projects environment vars, local, ci and deployments. Not sure exactly how this will work yet.
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.
Stuff will be here soon.
Some examples of the new code.
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;
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;
react, react-dom, and prop-types should probably be peer dependencies, and they should also probably be in dev dependencies, but not in dependencies.
Check for other dependency corrections as well while updating this.
@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.
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.
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>
)
}
interface Feature {
name: String,
isActive: false,
dependencies?: [...String]
}
([...Feature]) => [...String]
Takes an array of feature objects and returns an array of enabled feature names.
(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']
(...[...String]) => [...String]
Merge feature names without duplicating.
const currentFeatures = ['foo', 'bar', 'baz'];
mergeFeatures(currentFeatures, ['fish', 'bar', 'cat']); // ['foo', 'bar', 'baz', 'fish', 'cat']
([...String], [...String]) => [...String]
Removes feature names
const currentFeatures = ['foo', 'bar', 'baz', 'cat'];
deactivate(currentFeatures, ['fish', 'bar', 'cat']); // ['foo', 'baz']
(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
FeatureToggles is a provider component.
props
import { FeatureToggles } from '@paralleldrive/react-feature-toggles';
const features = ['foo', 'bar', 'baz', 'cat'];
const MyApp = () => {
return (
<FeatureToggles features={features}>
{.. stuff}
</FeatureToggles>
)
}
Feature
is a consumer component.
If the feature is enabled then the activeComponent will render else it renders the inactiveComponent.
Feature takes these props
Feature will pass these props to both the inactiveComponent and the activeComponent
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>
)
}
({ 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.
(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>
);
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>
)
}
See #25
part of #103
Create a new function called getBrowserQueryFeatures.
(search?) => [...String]
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']
after implementing getBrowserQueryFeatures and getReqQueryFeatures it might make sense to rename parseQuery to getQueryFeatures
Following the proposal outlined in #102
Implement update the following:
After these are implemented, we need to build more automation of query parsing and merging of features.
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
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
}
part of #103
(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']
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
because we have a function named mergeFeatures, I think removeFeatures makes more sense for this function name.
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.
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);
Is my brain broken, or does this example make no sense?
const Features = (withFeatures = { initialFeatures: [] }());
const Wrapper = () => <Features query={query} />;
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.
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.
part of #103
Write integration tests for withFeatures and configureFeatures
Will tackle this after #70
unit tests
integration tests
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.