Skip to main content

Developer Settings / Devtools

TVLK5 comes with a Devtools UI to help you modify certain behavior of the app to help with the testing.

By defaults Devtools UI is not visible to reduce distraction. You can call tdx.show() in the browser console to show the devtools UI. Note that you can only do this in dev and staging environment. In production the devtools renderer bundle is not shipped to the browser at all.

Creating your own devtools

Besides playing around with the toggles, you can create your own devtools to make your own UI more customizable. You can use it to filters certain search result, force error scenario, and so much more.

First you have to register it using useDevtools hooks. Each devtools should have a unique id. You can use anything but it's recommended to use your product as prefix in a camelCase format.

import { useDevtools } from '@traveloka/core/devtools';

function YourComponent() {
useDevtools({
id: 'myCustomDevtools',
title: 'My Custom Devtools',
sections: [],
});
}

You can then provide sections with a list of modifier type. Each section also should have a unique id. There are 4 of them

  • section. This is used to group things together, for example in Feature Control Devtools, each feature key is grouped inside single section. This type can contains multiple sections inside items object
  • toggle. Use this if you want to want to store boolean state.
  • input/multiline. Use this if you want to create free form text, usually the state is stored as string or object.
  • selection. Use this if you want the user to choose from predefined selection instead of free form input.
function Component() {
useDevtools({
id: 'myCustomDevtools',
title: 'My Custom Devtools',
sections: [
{
id: 'something',
title: 'Something',
type: 'section',
// set to true if you want all items inside a sections to be collapsed
// on initial render
// defaults to `false`
defaultCollapsed: true,
items: [
{
id: 'deep',
title: 'Deep',
type: 'multiline',
defaultValue: { a: true },
},
],
},
{
id: 'else',
title: 'Enable else',
type: 'toggle',
defaultValue: true,
},
],
});
}

This devtools config is also used to generate devtools state for the current devtools id. Using example above, the state becomes

const state = {
myCustomDevtools: {
something: {
deep: { a: true },
},
else: true,
},
};

useDevtools is a function with side effect where it automatically register your devtools inside an effect. It's recommended to put them in the topmost of your react tree. Typically you'd want to use this in React Context Provider level.

Dynamic Devtools Sections

You can generate sections dynamically based on props or state, for example in the case of Feature Control and A/B Test devtools we want to display all configs as different sections.

function Component() {
const stateResetKeys = props.list.map(item => item.id).sort();
useDevtools({
id: 'something',
title: 'Something',
sections: props.list.map(item => {
return {
id: item.id,
title: item.id,
items: [
{
...
}
]
}
})
}, stateResetKeys);

It's recommended to create a reset key (similar to how you define dependency in an effect), by passing an array in the second argument of useDevtools.

Reading Devtools State

If you need to read devtools state without having to register a custom devtools, you can use useDevtoolsState, by passing a state path string. You can read any devtools state (including the one registered in core modules)

import { useDevtoolsState } from '@traveloka/core/devtools';

function Component() {
const forceKey = useDevtoolsState('resource.contentKey');
if (forceKey) {
// do something
}
}

Devtools API

To access devtools API outside registration, you can use useDevtoolsAPI. This API doesn't have side-effect to register your current devtools, so it's safe to use this anywhere in your tree (doesn't have to be in the provider level).

Reacting to State Change

You can listen to the change of devtools state by creating a listener using api.subscribe method. this returns an unsubscribe function so you can easily return this from an effect

import { useDevtoolsAPI } from '@traveloka/core/devtools';

function Component() {
const api = useDevtoolsAPI();

useEffect(() => {
return api.subscribe('myCustomDevtools.something.deep', value => {
// do something with value
});
}, [api]);
}

You can choose to listen the change of state deep in the tree, or you can also choose to listen in the parent object. For example, if myCustomDevtools.something.deep changes, you can listen to any of these "events"

api.subscribe('myCustomDevtools', v => v.something.deep);
api.subscribe('myCustomDevtools.something', v => v.deep);
api.subscribe('myCustomDevtools.something.deep', v => v);

Update Devtools State

You can use api.updateState to update any devtools state. It accepts the same state path string as api.subscribe in the first argument.

api.updateState('myCustomDevtools.something.deep', false);
api.updateState('myCustomDevtools.something', { deep: false });
api.updateState('myCustomDevtools', { something: { deep: false } });

Make sure that you pass correct value if you decided to update the parent object.

Tree Modification

With Devtools API, you also able to modify react tree to insert, update, or remove custom react providers. Note that modifying react tree causes react to re-renders from scratch, so any useState calls will be reset.

import { useDevtoolsAPI } from '@traveloka/core/devtools';

function Component() {
const api = useDevtoolsAPI();

useEffect(() => {
const provider = <MyOwnProvider value="something" />;
return api.subscribe('key', v => {
if (v) {
api.upsertProvider(provider);
} else {
api.removeProvider(provider);
}
});
}, [api]);
}

Async Devtools Section

Even though you can create dynamic sections based on props, sometimes you need the value from an asynchronous operations, for example in A/B Test Devtools, we want to display current treatment inside section description. This value is fetched in client-side asynchronously, so we don't know the value initially when we set this up inside ABTestContext provider.

To do this, we can use api.dangerouslyUpdateSection which accepts a statePath string and a map section function. The statePath is required to prevent infinite setState loop as you can only updates single statePath exactly once.

import { useDevtoolsAPI } from '@traveloka/core/devtools';

function Component() {
const api = useDevtoolsAPI();

useEffect(() => {
api.dangerouslyUpdateSection('toolId.sectionId', section => {
return {
...section
items: section.items.concat(newItems)
}
});
}, []);
}