@traveloka/core/resource
This module is a utility to collect "resource query" from components, and fetch/resolve them from Content API.
Note that we have 3 kinds of resource query: "content resource (CR) query", "image resource (IR) query", and "image slider (IS) query". When we fetch CR and IR queries together, we call it a "batch resource query".
Important files
-
Fetchers
packages/core/resource/APIFetcher.ts— API fetchers for the usual casepackages/core/resource/APIFallbackFetcher.ts— API fetchers for fallback CR query (we fetch from AWS S3)packages/core/resource/getResourceData.ts— Entrypoint forgetStaticPage,getServerSideProps, andcacheableServerPropspages' data-fetcher to access CR/IR/IS APIs. (getInitialPropspages rely onresolveResourcefunction ofpackages/core/resource/ResourceResolverAndCollector.tsinstead)
-
Data structure
packages/core/resource/ResourceCache.ts— LRU store to store resource query result. The keys are in the format ifname.entrypackages/core/resource/ResourceCollectorProxy.ts— Treat this to contain just the proxy objects with extra utilities. It includes utilities to "assign" resource query result and return it as a simple object.
-
Hooks
packages/core/resource/ResourceContext.tsxpackages/core/resource/ResourceContext.client.tsx
-
Utilities
packages/core/resource/packages/core/resource/ResourceResolverAndCollector.ts— Contains utilities for the first render pass ofwithTravelokaPage. (TheresolveResourcefunction renders the component and collects the query through side effect, while thecollectXxxQueryfunctions which is called by theuseXxxResourcehooks collect the queries when the component is rendered.)
How this module works
GIP server-render flow
Refer to packages/core/next/withTravelokaPage.tsx.
During server prepass render
- Resource collector proxy is created
const resourceCollectorProxy = createResourceCollectorProxy();
- Page do an internal render pass with the
resolveResourcefunction.- The
ResourceCollectorProxypassed insidesharedPageProps.resourceis used to set the value ofResourceProviderduring this render. - Inside
useXxxResourcehooks, ifResourceProviderholds theResourceCollectorProxyproxy object (instead of the REAL resource data), it returns the result ofcollectContentQuery, which returns placeholder values (so that the component can have some data to render) AND ALSO modifies the internalresourceCollectorProxy.xxxQueryvalues to note which queries are used. - The
resolveResourcefunction fetches content API based on its internalresourceCollectorProxy.xxxQueryvalues. - The fetched value is stored in
ResourceCollectorProxyviaresourceCollectorProxy.assignResolvedXxxToCachefunctions, so thatresourceCollectorProxy.getData()will return the actual resource data
- The
const sharedPageProps = {
rawAppContext,
resource: resourceCollectorProxy
...pageProps,
};
resolveResource(
<DefaultRouterDataProvider ctx={ctx}>
<TravelokaApp Component={Page as any} pageProps={sharedPageProps} />
</DefaultRouterDataProvider>,
resourceProxy,
ctx
);
- The result is returned as part of the props
- Note that
resourceCollectorProxy.getData()depends on us callingresourceCollectorProxy.assignResolvedXxxToCachefunction first, so that it returns with the actual resource data.
- Note that
return {
rawAppContext,
resource: resourceCollectorProxy.getData(),
...pageProps,
};
During server render
ResourceProvider holds a plain resource object.
- Given
useXxxResource(shape)hook call, theuseXxxResourceconstructs a plainresultobject by inspecting the shape ofshapeand taking only the necessary values fromResourceCollectorProxy(the value that we fetched from server earlier). Thisresultobject is the resource data returned to the component.
GSP and GSSP server-render flow
Refer to packages/core/next/ssg/getSharedStaticProps.ts and packages/core/next/ssg/getSharedServerProps.ts.
This is simpler as we have listed all the listed resource needed when we call staticProps(...), or manually inside getSharedServerProps(ctx, ...resources)
-
getSharedStaticPropsandgetSharedServerPropsinternally callsgetResourceData(ctx, mergeResource(...resourceQuery))so that it can return theresourceobject in the returned "shared props". -
getResourceDatacreates acollectorfunction (this time, there is no prepass render as we should've listed all the required data), fetches the API, and assigns the data back to thecollectorobject. -
getResourceDatareturns the resource data viacollector.getData() -
The component renders with the actual resource data.
Client-render flow
Refer to packages/core/resource/ResourceContext.client.tsx.
Note that the value passed to ResourceProvider depends on the data-fetching function (see what object is passed to the resource shared props)
- GIP (
packages/core/next/withTravelokaPage.tsx): the value differs whether it's the render prepass (we passResourceCollectorProxy) or the normal render pass (we passresourceCollectorProxy.getData()) - GSP (
packages/core/next/ssg/getSharedStaticProps.ts): we pass the resource cache object - GSSP (
packages/core/next/ssg/getSharedServerProps.ts): we pass the resource cache object
Prepass (GIP page on client-navigation only)
sharedPageProps.resource initially contains proxy, so the following lines are triggered instead.
// During the first render (GIP page only), we use `ResourceCollectorProxy`
// Just collect the query to prevent request waterfall
if (collector !== null) {
return collectContentQuery(shape, collector);
}
It returns placeholder values (so that the component can have some data to render) AND ALSO modifies the internal resourceCollectorProxy.xxxQuery values to note which queries are used.
Normal pass
The following lines are triggered:
// For subsequent render, we return suspender to handle code-splitting
const fetchStatus = fetchXxxResource(shape, resourceCache.content);
return suspendsOrReturns(fetchStatus);
fetchXxxResource returns an object of { status, value }. The status could be "pending", "resolved", or "rejected". The value is a promise (if it is pending), the resource object (if it has resolved), or an error (if it rejects).
The internal working of fetchXxxResource is like this:
- It checks if all the requested query has been resolved. If so, it resolves immediately with the resource data.
- Otherwise, it checks if it has performed the same computation (query) before, and returns the result of that previous computation.
- Otherwise, it fetches the API and returns a promise value (while asynchronously updating the
cacheobject when the fetch has resolved)
Also note that due to the possibility of the suspendsOrReturns suspends if it encountered unresolved queries, components which might request additional queries in the client-side need to be wrapped in React <Suspense>, otherwise the page will break.