Feature flag (or feature toggle, feature switch…) comes with different shapes and implementations, however, is a well known and powerful technique for allowing teams to modify system behaviour without changing code.
The idea is to be able to enable or disable features during execution time without any deploy. That is a powerful tool with various implementations across different languages, the applications are many: A/B testing, toggle app configuration, delivery new features gradually, etc.
Storage
While having feature flags is very handy they introduce complexity starting by the management of the flags. That is one of the reasons why you will find managed services like Optimizely, Rollout, and others. In addition to the feature, they offer a lot more like analytics, and targeting.
It does not mean that you need a third-party app or integration to start using feature flags. In fact, every tool and approach should be considered depending on what do you need at the implementation moment.
You may opt-in for a managed service or manage your own feature flags in your own database as well.
API
So once you have decided how to manage the flags we need to expose them in our GraphQL API. The objective is always to strive for making your schema self contained and easy to understand, ideally, it should not reflect the way it is stored in the database (although it can).
We want to expose the features available for some user, app or instance. A query that provides that information could look like the following:
type Query {
enabledFeatures: [Feature!]!
}
type Feature {
name: String!
}
In the schema above, we are defining two important units in our domain, they are:
-
Feature
andenabledFeatures
.Feature
is a representation of the feature you want to switch on and off and it only contains aname
at the moment. -
enabledFeatures
is a query that returns an array ofFeature
.
We only return the features enabled so whoever is consuming the API does not need to know the entire set of features. The lack of some feature in the array means that the feature is not visible/available.
Note: In the backend, you can identify the user through headers such as Authorization and filter features for that user. Or filter based in any other information in the request, for example, User Agent.
You can see this schema being served live on this API sandbox. I'm using Apollo Server in the example.
Querying
Having the contract defined we are now able to fetch features. You can play around on the playground built in the sandbox example.
query {
enabledFeatures {
name
}
}
In the React application, I will be using Apollo Client as I'm familiar with the API, but it does not matter the library you use. You can follow along with the implementation of this client sandbox.
A component that queries all the features would look like the following:
const QUERY = gql`
query {
enabledFeatures {
name
}
}
`;
function BasicQuery() {
const { loading, error, data } = useQuery(QUERY);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :</p>;
return (
<div>
<h2>List of features:</h2>
<ul>
{data.enabledFeatures.map(feature => (
<li key={feature.name}>
<p>{feature.name}</p>
</li>
))}
</ul>
</div>
);
}
Having that is a good start point, right know you already have a way to query all the features and you can make use of it to dynamically turn something on and off. So let's make that.
useFeatureFlag
I want to guard a component so every time the user hit that part of the code we only render it if the feature is enabled. To avoid writing this logic over and over again I'm going to build a hook (previously I've used render props, but you can also make it a HOC component, you can work with whatever suits you prefer). The hook gets a feature name it checks if that is enabled and returns the status to the caller.
function useFeature(name) {
const { loading, error, data } = useQuery(QUERY);
let feature = {
loading,
error,
};
if (!data) return feature;
const enabled = data.enabledFeatures.some(feature => feature.name === name);
feature.enabled = enabled;
return feature;
}
That hook uses the same query we used before and it will return whether or not the passed name
is present in the list of features, as well as loading and error state if you want to handle the intermediate states. We can now use it in any component to switch the render output depending on it.
const Feature3 = () => {
const name = 'feature3';
const feature = useFeatureFlag(name);
if (feature.loading || feature.enabled === undefined) {
return <p>Loading {name}...</p>;
}
if (feature.error) return <p>Error :</p>;
if (feature.enabled) {
return <h2>{name} is enabled.</h2>;
}
return <h2>{name} is disabled.</h2>;
};
If we have only feature1
and feature2
enabled when querying feature3
we should see the disabled message. Similarly, if we query feature2
or feature1
we should see the enabled message.
Caching and better UX
Although our useFeatureFlag
is enough to define if a feature is enabled, it queries enabledFeatures
when the component is mounted. Depending on the application and the goal of your flag it can decrease the user experience because the user will have to wait for the query to finish.
Thankfully Apollo Client
comes by default with an in-memory cache implementation! Knowing that we deduce the useFeatureFlag
will be slower only on its first execution. After that, the result will be cached. However, we can go further and cache it ahead of time.
We can implement a pretty similar component to the BasicQuery
what would follow the same principles as useFeatureFlag
, but it is not concerned about any specific feature. It is only worried about querying them and rendering the children.
function FeatureFlags({ children }) {
const { loading, error } = useQuery(QUERY);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :</p>;
return <React.Fragment>{children}</React.Fragment>;
}
You may customize the way you render by ignoring the loading state or error, depending on your needs.
It works like context providers and you could indeed use the context API to create a provider to share its features and consume them with hooks. But this approach may be good enough as well.
FeatureFlags
could be placed in the top of your component tree or would wrap a portion of your App that can be controlled by the feature flags.
const App = () => (
<ApolloProvider client={client}>
<FeatureFlags>
<div className="App">
<h1>Hello Feature Flag</h1>
<section>
<BasicQuery />
</section>
<section>
<Feature2 />
</section>
<section>
<Feature3 />
</section>
</div>
</FeatureFlags>
</ApolloProvider>
);
This approach is not sophisticated, however already have some advantages like saving network calls being fired on every useFeatureFlag
. Which also avoids the pending state in every guarded component. The trade-off here is to slow down the overall load in favour of the later dependants rendering way faster.
The cache consistency can be a problem if we change the feature in the backend, but the user already cached the queries. For working around that you may extend the hook to receive a fetch policy option. Apollo allows you to configure the way you interact with the cache and you can opt-in for network-only
, for example. Nevertheless, the in-memory cache only lives until the page is refreshed it might not be that critical depending on your use case.
What is next?
That is my initial take when thinking about feature flags with GraphQL. The possibilities are many and as I wrote several times in this article it will depend on your use cases! So make it better work for you.
In my perception potential extensions would be:
- Adding options or variations as a field of
Feature
, then you can branch the feature implementation depending on its variation or properties (aka A/B testing). - Making the components and hooks more generic accepting the query as props to them.
- Adding a new query,
featureEnabled
to query theenabled
status by thename
directly from the backend so you do not have to filter it in the client-side.
What is your take on feature flags? 😊🙌🏽
cover image by Mikkel Bech on Unsplash
This post is a complete rewrite of feature flag approach for react using apollo client. I felt it needed an update.
Top comments (1)
This doesn't really take advantage of gql, you need to fetch all your flags even though you may just need one to render a page