Creates utility functions to store data in cache with stale-while-revalidate support. - Use `withCache.fetch` to simply fetch data from a third-party API. Fetches data from a URL and caches the result according to the strategy provided. When the response is not successful (e.g. status code >= 400), the caching is skipped automatically and the returned `data` is `null`. You can also prevent caching by using the `shouldCacheResponse` option and returning `false` from the function you pass in. For example, you might want to fetch data from a third-party GraphQL API but not cache the result if the GraphQL response body contains errors. - Use the more advanced `withCache.run` to execute any asynchronous operation. Utility function that executes asynchronous operations and caches the result according to the strategy provided. Use this to do any type of asynchronous operation where `withCache.fetch` is insufficient. For example, when making multiple calls to a third-party API where the result of all of them needs to be cached under the same cache key. Whatever data is returned from the `fn` will be cached according to the strategy provided. > Note: > To prevent caching the result you must throw an error. Otherwise, the result will be cached. > For example, if you call `fetch` but the response is not successful (e.g. status code >= 400), > you should throw an error. Otherwise, the response will be cached.
// In your app's `server.ts` file:
import * as remixBuild from '@remix-run/dev/server-build';
import {createWithCache, CacheLong} from '@shopify/hydrogen';
// Use another `createRequestHandler` if deploying off oxygen
import {createRequestHandler} from '@shopify/remix-oxygen';
export default {
async fetch(request, env, executionContext) {
const cache = await caches.open('my-cms');
const withCache = createWithCache({
cache,
waitUntil: executionContext.waitUntil.bind(executionContext),
request,
});
// 1. Create a custom utility to query a third-party API:
const fetchMyCMS = async (query) => {
const {data, response} = await withCache.fetch(
'https://my-cms.com/api',
{
method: 'POST',
body: query,
headers: {Authorization: 'Bearer 123'},
},
{
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no data errors or a specific data that make this result not suited for caching
shouldCacheResponse: (result) =>
!(result?.errors || result?.isLoggedIn),
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
displayName: 'My CMS query',
},
);
// Access the response properties:
console.log(data, response.headers);
return data;
};
// 2. Or Create a more advanced utility to query multiple APIs under the same cache key:
const fetchMultipleCMS = (options) => {
// Prefix the cache key and make it unique based on arguments.
return withCache.run(
{
// Define a cache key that is unique to this query
cacheKey: ['my-cms-composite', options.id, options.handle],
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no data errors or a specific data that make this result not suited for caching
shouldCacheResponse: (result) =>
!(result?.errors || result?.isLoggedIn),
},
async (params) => {
// Run multiple subrequests in parallel, or any other async operations.
const [response1, response2] = await Promise.all([
fetch('https://my-cms-1.com/api', {
method: 'POST',
body: JSON.stringify({id: options.id}),
}),
fetch('https://my-cms-2.com/api', {
method: 'POST',
body: JSON.stringify({handle: options.handle}),
}),
]);
// Throw if any response is unsuccessful.
// This is important to prevent the results from being cached.
if (!response1.ok || !response2.ok) {
throw new Error('Failed to fetch data');
}
const [data1, data2] = await Promise.all([
response1.json(),
response2.json(),
]);
// Validate data and throw to avoid caching errors.
if (data1.errors || data2.errors) {
throw new Error('API errors');
}
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
params.addDebugData({displayName: 'My CMS query'});
// Compose the result as needed.
return {
...data1,
...data2,
extra1: response1.headers.get('X-Extra'),
extra2: response2.headers.get('X-Extra'),
};
},
);
};
const handleRequest = createRequestHandler({
build: remixBuild,
mode: process.env.NODE_ENV,
getLoadContext: () => ({
// Make sure to update env.d.ts to
// include these properties in `AppLoadContext`.
fetchMyCMS,
fetchMultipleCMS,
}),
});
return handleRequest(request);
},
};
// In your app's `server.ts` file:
import * as remixBuild from '@remix-run/dev/server-build';
import {createWithCache, CacheLong} from '@shopify/hydrogen';
// Use another `createRequestHandler` if deploying off oxygen
import {createRequestHandler} from '@shopify/remix-oxygen';
export default {
async fetch(
request: Request,
env: Record<string, string>,
executionContext: ExecutionContext,
) {
const cache = await caches.open('my-cms');
const withCache = createWithCache({
cache,
waitUntil: executionContext.waitUntil.bind(executionContext),
request,
});
type ExpectedResponse = {
content: unknown;
isLoggedIn: boolean;
errors?: string;
};
type MergedResponse = {
content: unknown;
isLoggedIn: boolean;
errors?: string;
extra1: string | null;
extra2: string | null;
};
// 1. Create a custom utility to query a third-party API:
const fetchMyCMS = async (query: string) => {
const {data, response} = await withCache.fetch<ExpectedResponse>(
'https://my-cms.com/api',
{
method: 'POST',
body: query,
headers: {Authorization: 'Bearer 123'},
},
{
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no data errors or a specific data that make this result not suited for caching
shouldCacheResponse: (result) =>
!(result?.errors || result?.isLoggedIn),
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
displayName: 'My CMS query',
},
);
// Access the response properties:
console.log(data, response.headers);
return data;
};
// 2. Or Create a more advanced utility to query multiple APIs under the same cache key:
const fetchMultipleCMS = (options: {id: string; handle: string}) => {
// Prefix the cache key and make it unique based on arguments.
return withCache.run(
{
// Define a cache key that is unique to this query
cacheKey: ['my-cms-composite', options.id, options.handle],
// Optionally, specify a cache strategy.
// Default is CacheShort().
cacheStrategy: CacheLong(),
// Cache if there are no data errors or a specific data that make this result not suited for caching
shouldCacheResult: (result: MergedResponse) =>
!(result?.errors || result?.isLoggedIn),
},
async (params) => {
// Run multiple subrequests in parallel, or any other async operations.
const [response1, response2] = await Promise.all([
fetch('https://my-cms-1.com/api', {
method: 'POST',
body: JSON.stringify({id: options.id}),
}),
fetch('https://my-cms-2.com/api', {
method: 'POST',
body: JSON.stringify({handle: options.handle}),
}),
]);
// Throw if any response is unsuccessful.
// This is important to prevent the results from being cached.
if (!response1.ok || !response2.ok) {
throw new Error('Failed to fetch data');
}
const [data1, data2] = (await Promise.all([
response1.json(),
response2.json(),
])) as [ExpectedResponse, ExpectedResponse];
// Validate data and throw to avoid caching errors.
if (data1.errors || data2.errors) {
throw new Error('API errors');
}
// Optionally, add extra information to show
// in the Subrequest Profiler utility.
params.addDebugData({displayName: 'My CMS query'});
// Compose the result as needed.
return {
...data1,
...data2,
extra1: response1.headers.get('X-Extra'),
extra2: response2.headers.get('X-Extra'),
} as MergedResponse;
},
);
};
const handleRequest = createRequestHandler({
build: remixBuild,
mode: process.env.NODE_ENV,
getLoadContext: () => ({
// Make sure to update env.d.ts to
// include these properties in `AppLoadContext`.
fetchMyCMS,
fetchMultipleCMS,
}),
});
return handleRequest(request);
},
};
cacheOptions: CreateWithCacheOptions
export function createWithCache( cacheOptions: CreateWithCacheOptions, ): WithCache { const {cache, waitUntil, request} = cacheOptions; return { run: <T>( {cacheKey, cacheStrategy, shouldCacheResult}: WithCacheRunOptions<T>, fn: ({addDebugData}: CacheActionFunctionParam) => T | Promise<T>, ): Promise<T> => { return runWithCache(cacheKey, fn, { shouldCacheResult, strategy: cacheStrategy, cacheInstance: cache, waitUntil, debugInfo: { ...getDebugHeaders(request), stackInfo: getCallerStackLine?.(), }, }); }, fetch: <T>( url: string, requestInit: RequestInit, options: WithCacheFetchOptions<T>, ): Promise<{data: T | null; response: Response}> => { return fetchWithServerCache<T | null>(url, requestInit ?? {}, { waitUntil, cacheKey: [url, requestInit], cacheInstance: cache, debugInfo: { url, ...getDebugHeaders(request), stackInfo: getCallerStackLine?.(), displayName: options?.displayName, }, cache: options.cacheStrategy, ...options, }).then(([data, response]) => ({data, response})); }, }; }
An instance that implements the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache)
The `request` object is used by the Subrequest profiler, and to access certain headers for debugging
The `waitUntil` function is used to keep the current request/response lifecycle alive even after a response has been sent. It should be provided by your platform.
The cache key for this fetch
Use the `CachingStrategy` to define a custom caching mechanism for your data. Or use one of the pre-defined caching strategies: [`CacheNone`](/docs/api/hydrogen/utilities/cachenone), [`CacheShort`](/docs/api/hydrogen/utilities/cacheshort), [`CacheLong`](/docs/api/hydrogen/utilities/cachelong).
Useful to avoid e.g. caching a successful response that contains an error in the body
The cache key is used to uniquely identify a value in the cache.
string | readonly unknown[]
Use the `CachingStrategy` to define a custom caching mechanism for your data. Or use one of the pre-defined caching strategies: CacheNone, CacheShort, CacheLong.
The maximum amount of time in seconds that a resource will be considered fresh. See `max-age` in the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#:~:text=Response%20Directives-,max%2Dage,-The%20max%2Dage).
The caching mode, generally `public`, `private`, or `no-store`.
Similar to `maxAge` but specific to shared caches. See `s-maxage` in the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#s-maxage).
Indicate that the cache should serve the stale response if an error occurs while revalidating the cache. See `stale-if-error` in the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-if-error).
Indicate that the cache should serve the stale response in the background while revalidating the cache. See `stale-while-revalidate` in the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#stale-while-revalidate).
The cache key for this run
Use the `CachingStrategy` to define a custom caching mechanism for your data. Or use one of the pre-defined caching strategies: [`CacheNone`](/docs/api/hydrogen/utilities/cachenone), [`CacheShort`](/docs/api/hydrogen/utilities/cacheshort), [`CacheLong`](/docs/api/hydrogen/utilities/cachelong).
Useful to avoid accidentally caching bad results