Skip to main content

Feature Control

note

If you're coming from nodejs-web (our legacy service), you might find inconsistent behaviors regarding the existing feature keys. This is because there was a bug on nodejs-web (that we have already too much relied on) where we use the DESKTOP_WEB feature keys for both desktop and mobile, and the cost of changing this behavior in nodejs-web is too high.

When you launch new features, you typically don't want them to be available publicly by default. This is where you separate between deployment (updating servers with new code) and release (enable the new code to public), so you can deploy often and release gradually.

The useFeatureControl hook

useFeatureControl

We provide the useFeatureControl hook that you can call using with feature key string. It returns a FeatureControl object containing enabled and properties field. Most of the time, you only need to care about the enabled property to check whether this particular feature is enabled or not.

import React from 'react';
import { useFeatureControl } from '@traveloka/core';

function Page() {
const feature = useFeatureControl('newPage');

if (feature.enabled) {
return <div />;
}

return <span />;
}

useGlobalFeatureControl

If somehow you need to access all the available feature control data, you can use the useGlobalFeatureControl hook.

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { useGlobalFeatureControl } from '@traveloka/core';

function useMCPQuery() {
const featureControl = useGlobalFeatureControl();
const payload = { featureControl };
return useQuery('QUERY_MCP', () => fetchMCPData(payload));
}

Accessing feature control on data-fetching functions

getInitialProps

note

This is the pattern for our legacy pages. See the next section for the newer technology.

We also provide a way to get feature control data in your page getInitialProps method. It's passed as 2nd argument as featureControl property inside appContext. Instead of Record<string, Feature> you'll get FeatureControl instance which is similar to JavaScript Map.

import React from 'react';

const Page = () => <div />;

Page.getInitialProps = (ctx, appContext) => {
const { featureControl } = appContext;
const feature = featureControl.get('newPage');

if (feature.enabled) {
return {};
}

return {};
};

Even though technically you can pass feature control value from getInitialProps to component render method, we strongly suggest to keep using useFeatureControl hooks.

getStaticProps

note

You should now use the staticProps helper instead of directly calling getSharedStaticProps. In this case, rawAppContext is available as the second argument of staticPropsOpts.getStaticProps.

If you're using SSG, feature control is fetched in build time (except when fallback is used). This means you can't rely on user session/dynamic feature control value based on runtime. With SSG mode, you can access feature control data inside getStaticProps or getServerSideProps, by accessing rawAppContext.featureControl property.

Unlike previous feature control method where you can immediately access the enabled property, here you have to check the existence of the feature control itself because it's a raw data.

export const getStaticProps = async ctx => {
const sharedPageProps = await getSharedStaticProps(ctx, pathname);
const { featureControl } = sharedPageProps.rawAppContext;

// use optional chaining to access `enabled` property
if (featureControl['newPage']?.enabled) {
return {
props: {
...sharedPageProps,
},
};
}

// including when accessing `properties` value
if (featureControl['newPage']?.properties?.something) {
return {
props: {
...sharedPageProps,
},
};
}

return {
props: {
...sharedPageProps,
},
};
};

Data fetching and Code Splitting

One of the use case of using feature control is having page level rewrite. In this case we suggest doing code splitting for page component. This way user is not burdened with loading asset for both variant.

Sometimes, different page might require different data, the API can also different. In that case there's a getInitialProps function that you can use you can still define .getInitialProps in both your page variation, and call it in the page component.

import React from 'react';
import dynamic from 'next/dynamic';
import { useFeatureControl, getInitialProps } from '@traveloka/core';

// code split page component so user only loads what they use
const OldPage = dynamic(import('./OldPage'));
const NewPage = dynamic(import('./NewPage'));

// The page component itself is simply doing conditional rendering based on
// feature control value.
const Page: TravelokaPageComponent = props => {
const feature = useFeatureControl('newPage');

// Don't forget to spread props because it comes from getInitialProps!
if (feature.enabled) {
return <NewPage {...props} />;
}

return <OldPage {...props} />;
};

// we need this to call different getInitialProps based on feature control value
Page.getInitialProps = async (ctx, appContext) => {
const { featureControl } = appContext;
const feature = featureControl.get('newPage');

// Technically we can call getInitialProps from page variation directly,
// it just would've been clearer using this helper function.
// See the alternative below:
//
// return import('./NewPage)
// .then(m => m.default.getInitialProps
// ? m.default.getInitialProps(ctx, appContext)
// : {}
// )
if (feature.enabled) {
return getInitialProps(await import('./NewPage'));
}

return getInitialProps(await import('./OldPage'));
};

Performance Optimization Caveat

To improve page performance, by default the framework will remove all unused feature controls. Any component using feature control that is not mounted on server / first-render will have false and empty properties by default because the framework doesn't know which component are actually rendered in client-side.

useDeferredFeatureControl

To fix this issue, you can use useDeferredFeatureControl hooks that will marks few feature control key as used. Make sure you call this in component that always rendered server-side.

This hooks accept variadic arguments containing feature control keys. Don't forget to add comment on why this feature control key is needed.

function ParentComponent() {
useDeferredFeatureControl(
// this is used in ChildWithFeatureKey1
'feature-key-1',
// this is used in ChildWithFeatureKey2
'feature-key-2',
// this is used in both components, but not rendered client-side
// because we use Modal
'product-modal'
);

if (someCondition) {
return <ChildWithFeatureKey1 />;
}

return <ChildWithFeatureKey2 />;
}

Note that you still need to use useFeatureControl to access the value inside child components

function ChildWithFeatureKey1() {
const feature = useFeatureControl('feature-key-1');
return <View />;
}

function ChildWithFeatureKey2() {
const feature = useFeatureControl('feature-key-2');
return <View />;
}