👋 Hi! I've recently been looking in how we can best support third-party integrations in Hydrogen. While Hydrogen is typically focused on serving components from our own first-party storefront API, most headless merchants still end up needing to integrate with third-party services. I've been looking into how we can be as opinionated as possible when it comes to this integration so that we can offer much better performance & ergonomics than manually fetch
ing an external API on the client and massaging all the data yourself.
We've been documenting some thoughts about how we can best achieve that over here, but ultimately we think we can add the most value to fetching third-party data with three things: caching, batching, and mapping. Why these three strategies?
- Caching should significantly increase performance when dealing with third-party data by colocating it alongside the Hydrogen runtime and would be a nice value-add for running Hydrogen on Oxygen.
- Batching all GraphQL requests to a given service would also reduce the number of waterfalling network requests needed to render a given page.
- Mapping foreign keys in a third-party datastore to the results returned from the SF API can be really tedious and requires a lot of glue code we can remove if we set have some default assumptions about how data is stored for given third-parties.
This idea in the form of a code example illustrates what the ergonomics of all server fetches in Hydrogen could look like if you squinted:
const query = gql`
query product($handle: String!) {
product(handle: $handle) {
id
}
}
`;
// These three shop queries are batched across hook calls (or across components within a Suspense boundary) into a single GraphQL request
const {data: data1} = useBatchedGraphQLQuery(query, { handle });
// Duplicate query detected while batching, no need to perform additional work & hashes to the same cache key
const {data: data2} = useBatchedGraphQLQuery(query, { handle });
// Cache-aware batching crafts the smallest request necessary, incorporates individual cache hits and makes use of H2 caching strategy
const {data: data3} = useBatchedGraphQLQuery(query, { handle: 'foo' }, {cache: { maxAge: 10 }});
const selectionSet = gql`{
title
upc
}; `
// Takes an existing Shopify product record and request data mapped to it via foreign key in external CMS
const {data: foo1} = useBatchedRecordsQuery(data.product, selectionSet, new SanityService(...));
// Batches many records with the same selection set within the same query,
// alongside other batched queries to the same service, while caching some records differentially
const {data: foo2} = useBatchedRecordsQuery(
{id: 'gid://shopify/Product/673085082834431'},
selectionSet,
new SanityService(...),
{ cache: { maxAge: 1} },
);
There's even a "working" prototype with comments for something similar if you want do just dive right in! But in trying to implement this, a few complications arose, and they mostly center around Hydrogen's use of the react-query
library for the useQuery
hook.
We're currently using react-query
(and previously useServerFetch
) to wrap & schedule promises for execution so that we can render React components with the data they need on the server. Rendering React components asynchronously isn't supported, so the need of these sorts of hooks for fetching data on the server before rendering definitely seems reasonable.
Some of the reasons we originally adopted react-query
was that it provided a more robust implementation for server-side requests that work with Suspense, that it works on the client, and that it comes with a built-in cache. However, some of the now-known drawbacks of adopting it as a library are that it doesn't support async fetches, and that the headers and other information about the response aren't being exposed.
One more drawback that the map+cache+batch prototype uncovered is that useQuery
absolutely won't allow for batching GraphQL requests, or delaying any async operation across hook invocations for that matter. react-query
includes its own scheduler to resolve promises as eagerly as possible and offers no configurability for deferring execution across hooks. If proper batching support were to be considered a priority, some of the options we have would be to either try to introduce batching support upstream in a library that wasn't designed for it, maintain our fork of react-query
with batching support, or return to writing our own implementation à la useServerFetch
.
Although things sound dire, being in control of our own custom hook for server fetches (again) could be beneficial for us:
react-query
has it's own separate layer of caching that could stand to be consolidated with the rest of our caching framework for simplicity & debuggability.
- In a RSC-world, support for client-side queries isn't something that we should necessarily strive to maintain.
- The library adds significant complexity and overhead for what effectively amount to a fetch call (at least when it comes to how we're using its features in Hydrogen)
- No proof-of-concept for this yet, but we should be able to automatically batch all GraphQL requests originating from anyt/all components within distinct Suspense boundaries.
I think it could be interesting to entertain the idea of building our own server-side data-fetching hook. The problem has been solved before: react-frontload
is a tiny library that could server as inspiration for building one of these pseudo-synchronous hooks. Looking for feedback as to whether folks think about something like this!
All this being said, as we try to converge with RSC, it's unclear whether building a hook would be necessary for server fetches in the future, or if just a general data-fetching library will suffice.
Although data-fetching in an RSC world doesn't seem to have been finalized, all examples point to the ability to make use of asynchronous libraries for data-fetching in server components (eg. db.notes.get(props.id)
in the official RFC, and a new React server renderer that supports Suspense and waiting on asynchronous data on the server without double-rendering).
I'm relatively new to the world of React so I may be overlooking a couple things. Looking for some feedback on the general appetite for replacing useReactQuery
, and also hoping this issue sparks some discussion about our server-side data-fetching patterns, both for now and in the future with RSC.