Skip to main content

Resource Query

When building the app, we're not writing text strins manually because different locales might use different sentences. They may also have different types of images as well.

To accommodate the 3 types of resource data that we can use, we call them: content resource, image resource, and image slider.

Content Resource

This is the source of truth of our translation data.

Content resources are grouped by "name". Each content resource "name" can have multiple "keys". Think of "name" as the table name in database and the "key" as its fields.

Accessing content resource

To get content resource data, we can use the useContentResource hook that accepts the content resource shape/query. It will return exactly the shape that you requested.

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

function Component() {
const cr = useContentResource({
GeneralLayout: {
logIn: '',
},
});

return <span>{cr.GeneralLayout.logIn}</span>;
}

Formatting ICU messages

Content resource can return a string with the ICU Message format. This is useful if you want to combine the string with external data such as from API response or user inputs. To format an ICU message, you can use the useICUFormatter hook.

Alternatively, you can do it yourself using useLocale and the intl-messageformat npm package.

import React from 'react';
import {
useLocale,
useContentResource,
useICUFormatter,
} from '@traveloka/core';

function Component() {
const formatICU = useICUFormatter();
const cr = useContentResource({
HotelSearch: {
searchAccom: '',
},
});

const label = formatICU(cr.HotelSearch.searchAccom, { accomType: 'HOTEL' });
return <span>{label}</span>;
}

Rendering HTML from content resource

Sometimes, a content resource key also returns a string containing HTML elements.

Because we're using React, any HTML you pass as children will be escaped. If you insist of rendering HTML string, we recommend using htmr due to its small client bundle size.

Note that this module does not guarantee any security protection, especially if you combine this with string formatting using user inputs (which is insecure by default).

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

function Component() {
const cr = useContentResource({
popularTop: {
popularActivity: '',
},
});

return htmr(cr.popularTop.popularActivity);
}

Image Resource

Similar to content resource, image resource takes in the form of "name" and "key". The difference is instead of a string, each key represent a ImageResourceData as follows:

type ImageResourceData = {
activeTimeMsec: null | number;
activeTimeTZOffsetMinutes: null | number;
altText: string;
captionText: string;
expireTimeMsec: null | number;
expireTimeTZOffsetMinutes: null | number;
key: string;
link: string;
linkTo: string;
linkUrl: string;
order: number;
path: string;
titleText: string;
type: 'AMAZON_S3' | string;
};

Accesing image resource

To access an image resource, you use a method similar to that for accessing a content resource. Instead of useContentResource, we use the useImageResource hook and instead of passing an empty string, we pass an object containing properties of ImageResourceData that we want to access.

To reduce bandwidth and memory usage, even though our API returns the full property of ImageResourceData, we only hydrate the ones that you requested. That's why if you don't specify some properties, you will not be able to access them in your code.

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

function Component() {
const ir = useImageResource({
AlternativeAccomEntry: {
apartments: {
link: '',
},
},
});

return <img src={ir.AlternativeAccomEntry.apartments.link} />;
}

Image Slider

If you need a list of images, you can use the image slider. Image slider only has the "name" property—there's no "key" because it returns multiple images at once.

Each image slider item has all the properties available in ImageResourceData, as well as additional properties specific to image sliders. The type definition is as follows:

type ImageSliderData = ImageResourceData & {
descriptionBottom: null | string;
descriptionOverlay: null | string;
groupName: string;
titleBottom: null | string;
titleOverlay: null | string;
};

Accessing image slider

Similar to image resource, you need to pass the properties that you want to access in the query.

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

function Component() {
const sl = useImageSlider({
HomeUSP: {
link: '',
altText: '',
},
});

return sl.HomeUSP.map(img => <img src={img.link} alt={img.altText} />);
}

Lazy loading via Suspense

info

The documentation of how @traveloka/core/reosurce works is documented in the "core reference" section.

Those 3 hooks above supports using Suspense data fetching in the client-side.

This means that if you have conditional rendering (or lazy loading) in the client-side based on some state, re-rendering some subtree might the trigger loading state if the newly requested resource has not been fetched before.

To handle the loading state, you can use the Suspense component that accepts fallback as props. Make sure that this components is rendered exclusively in the client-side.

import React, { Suspense } from 'react';

function Component() {
const [isTrue, setTrue] = React.useState(true);

return (
<Suspense fallback={<Loading />}>{isTrue ? <Truth /> : <Lie />}</Suspense>
);
}

Performance Optimization via useResource

As previously stated, lazy/conditional data fetching is handled using Suspense, which means each of these hooks can suspend rendering their subtree (if any resource query has not been fetched before). It also means that re-rendering subtree might trigger multiple requests in a series.

Fetching resources in a single pass with useResource

Consider this example (assume Component is rendered inside a conditional branch):

import React from 'react';
import { useContentResource, useImageSlider } from '@traveloka/core';

function Component() {
const cr = useContentResource({
GeneralLayout: {
logIn: '',
},
});
const sl = useImageSlider({
HomeUSP: {
link: '',
},
});

return <div />;
}

In this example, useImageSlider won't be executed until the Suspense from useContentResource has been resolved. We are waiting for an API call to fetch content resource, and only then we wait for another API call to fetch image slider.

To fix that, we provide useResource hooks that allow you to fetch multiple resource queries in a single pass.

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

function Component() {
const resource = useResource({
contentResource: {
GeneralLayout: {
logIn: '',
},
},
imageSlider: {
HomeUSP: {
link: '',
},
},
});

// Access those resources here:
// - `resource.contentResource.GeneralLayout.logIn`
// - `resource.imageSlider.HomeUSP[0].link`
return <div />;
}

Avoiding waterfall fetching by merging resource queries

The above example still doesn't handle cases where we get waterfall API request if you have a lot of conditional branch due to state changes. Unfortunately this is something that you need to get around manually.

Our general rule is to lift your query up. If you know the resource queries of your child in advance, you can place it in the parent component instead.

Either you hardcode the query, or the child can export their query and you merge it with parent query. You can use the mergeCR (for content resource), mergeIR (for image resource), mergeIS (for image slider), or the mergeResource utility functions.

// ================================================================================
// Parent.tsx
// ================================================================================
import React from 'react';
import { useContentResource, mergeCR } from '@traveloka/core';
import Child, { contentQuery as childContentQuery } from './Child';

const parentContentQuery = {
SimpleSentences: {
cancel: '',
},
};

const contentQuery = mergeCR(parentContentQuery, childContentQuery);

function Parent() {
const cr = useContentResource(contentQuery);

return (
<>
<div>{cr.SimpleSentences.cancel}</div>
<Child />
</>
);
}

// ================================================================================
// Child.tsx
// ================================================================================
import React from 'react';

export const contentQuery = {
SimpleSentences: {
done: '',
},
};

export default function Child() {
// It's safe to call useContentResource here because when parent finished
// fetching data, child query will already cached so this doesn't trigger
// another API call. Using this method allow us to not pass resource object
// to child component
const cr = useContentResource(contentQuery);

return <div>{cr.SimpleSentences.done}</div>;
}