In this post, I'll show you how to implement feature management on your React applications. Feature management is the way to control which features are available based on certain scenarios.
Why Do Feature Management?
Here are some scenarios where controlling feature availability is necessary:
- The feature should only be turned on after a marketing event. With feature flags, the feature can already be sitting in production and an administrator can just simply enable it via configuration once the event is done.
- The feature should only be available to users on a specific plan.
All of these scenarios share the same concept. In this post, we'll focus on the 2nd one. Let's go!
The Requirements
Let's say we're building a blog application that offers the ff. features:
- Updating Title and Content
- Publishing to multiple blog platforms
- Scheduling the publish time
Now, let's say the app offers 3 plans to users: Basic, Pro, and, Ultimate.
With these plans, the team has decided to have a plan comparison table that looks like this:
Feature | Basic | Pro | Ultimate |
Update Title and Content | Yes | Yes | Yes |
Schedule the publish time | No | Yes | Yes |
Publish to multiple blog platforms | No | No | Yes |
We now have all the requirements defined. Let's start building!
The Starting Point
For your reference, the initial state of the project can be found here.
For simplicity, we only have the New Blog
page in this version.
The page contains the following:
- A Role Switcher to simulate changing of the plans
- All the features are still available to the user
Here's how it looks like at this point:
The Role Switcher
Here's a look at the RoleSwitcher
component:
function RoleSwitcher({ plans, activePlan, setActivePlan }) {
const navItemTags = () =>
plans.map((label, index) =>
<li className="nav-item" key={index}>
<button className={"nav-link" + (activePlan === index ? " active" : "")} onClick={e => setActivePlan(index)}>{label}</button>
</li>
);
return (
<ul className="nav nav-pills">
{navItemTags()}
</ul>
);
};
The component receives 3 properties which are passed by the App
component.
function App() {
const availablePlans = Config.availablePlans;
const [activePlan, setActivePlan] = useState(0);
<RoleSwitcher
plans={availablePlans} activePlan={activePlan}
setActivePlan={setActivePlan} />
The App
component then refers to the availablePlans
exposed by Config.js
const Config = {
availablePlans: ['Basic', 'Pro', 'Ultimate']
};
The RoleSwitcher
displays all the available plans and allows the user to set the activePlan
. The activePlan
variable will then be passed to the Editor
component later on.
The Feature-Based Approach
Now, let's update the other features to only be shown on the appropriate plan.
In this approach, we let the plans identify the features that are going to be available in them.
Therefore, we need to make changes to the Config
file:
const Config = {
availablePlans: [
{
id: 0,
name: 'Basic',
features: []
},
{
id: 1,
name: 'Pro',
features: ['scheduledPost']
},
{
id: 2,
name: 'Ultimate',
features: ['scheduledPost', 'multiplePublishers']
}
]
};
In the above change, each plan inside the availablePlans
array now has a list of features
that are available in them.
Then, let's update the RoleSwitcher
to support this new format:
function RoleSwitcher({ plans, activePlan, setActivePlan }) {
const navItemTags = () =>
plans.map((plan) =>
<li className="nav-item" key={plan.id}>
<button
className={"nav-link" + (activePlan.id === plan.id ? " active" : "")}
onClick={e => setActivePlan(plan)}>{plan.name}</button>
</li>
);
return (
<ul className="nav nav-pills">
{navItemTags()}
</ul>
);
};
The initialization section of the App
component also needs to be updated:
const availablePlans = Config.availablePlans;
const [activePlan, setActivePlan] = useState(availablePlans[0]);
And now, for the star of this post, let's talk about the FeatureBlock
component!
The Feature Block Component
function FeatureBlock(props) {
const isFeatureVisible = props.activePlan.features
.find(feature => feature === props.featureName);
if (isFeatureVisible) {
return (
<>
{props.children}
</>
);
} else {
return null;
}
};
The FeatureBlock
component is a wrapper component and receives the following:
- The
activePlan
property to determine the selected plan - The
featureName
property to which will be matched against thefeatures
list of theactivePlan
property. If a match is found, theFeatureBlock
's children will be rendered. - The component's children which are conditionally rendered based on the above 2 properties
For simplicity, we'll only use the FeatureBlock
component to wrap the publisher and scheduled inputs in the Editor
component. This is because the title and content fields are available to all plans anyway.
The Editor
component will now have this structure:
function Editor({ activePlan }) {
return (
<div className="col col-12">
<h1>New Post</h1>
<!--Title and content fields here-->
<FeatureBlock activePlan={activePlan}
featureName="multiplePublishers">
<!--The publishers selection here-->
</FeatureBlock>
<FeatureBlock activePlan={activePlan}
featureName="scheduledPost">
<!--The schedule input here-->
</FeatureBlock>
<!--Save and submit buttons here-->
);
}
After all these changes, the RoleSwitcher
will now toggle the visibility of the publisher and schedule inputs.
As you can see, everything is functioning as expected. But, there's a problem with the Config
file.
It's too centralized! Once the app grows, the Config
file has the potential to be bloated. Let's solve this in the next section.
By the way, here's the code at this point.
The Plan-Based Approach
To solve the problem with the Config
file, we should follow a plan-based approach.
In this approach, we're inverting the dependency. Instead of letting the plan define the features available, we let the features specify which plans they should be available on. This is a more modular and cleaner approach.
The Editor
component will now have this structure:
function Editor({ activePlan }) {
return (
<div className="col col-12">
<h1>New Post</h1>
<!--Title and content fields here-->
<FeatureBlock activePlan={activePlan}
planNames={["Ultimate"]}>
<!--The publishers selection here-->
</FeatureBlock>
<FeatureBlock activePlan={activePlan}
planNames={["Pro", "Ultimate"]}>
<!--The schedule input here-->
</FeatureBlock>
<!--Save and submit buttons here-->
);
}
The FeatureBlock
will also be updated to support this change:
function FeatureBlock(props) {
const isFeatureVisible = props.planNames.find(plan =>
plan === props.activePlan.name);
if (isFeatureVisible) {
return (
<>
{props.children}
</>
);
} else {
return null;
}
};
Now that the Config
will not be responsible for storing the plan-to-feature mappings anymore, it can already be simplified to:
const Config = {
availablePlans: [
{
id: 0,
name: 'Basic'
},
{
id: 1,
name: 'Pro'
},
{
id: 2,
name: 'Ultimate'
}
]
};
Here's the code at this point.
In Summary
We've successfully implemented Feature Management in React!
What I've provided is simply a skeletal reference. You can expand it further to support more advanced requirements.
In my case, I was thinking to implement user-specific feature availability. In this scenario, specific features can only be available to users who pass certain conditions.
For example, what if only want to make the scheduled publishing feature available to users from a certain location? We'll explore this in my next article which I'll upload in the next few days!
Glad that you've reached the end of this post. Let me know what you think of this approach by sending in your comments.
I hoped you learned something new from me today!
Hey, you! Follow me on Twitter!