Remix Deferred is currently implemented on top of React's Suspense model but is not limited to React. This will be a quick dive into how "promise over the wire" is accomplished.
It isn't rocket science, but a quick recap of how frameworks such as react do SSR:
- Load data
- Render the app
- Serialize the data
// 1. load data
const data = await loadData(request);
// 2. render app
const appHtml = toString(<App data={data} />);
response.end(
`
<!DOCTYPE>
<html>
<div id="app">
${appHtml}
</div>
<!-- 3. serialize data -->
<script>window.__data = ${JSON.stringify(data)};</script>
<script src="entry.client.js"></script>
</html>
`
);
Then on the client side, hydration occurs by reading the serialized data from the window:
const data = window.__data;
hydrate(document.getElementById("app"), <App data={data} />);
Simple yet effective.
Remix deferred operates on the same general pattern as your standard SSR + Hydration setup, with the caveat of if a promise is returned as a data key, it "teleports" over the network and arrives in the browser also as a promise.
A high level of how this is accomplished can be understood by the following snippet:
- Load data
- Render the app
- Separate critical & deferred data
- Serialize critical data
- Teleport promises
- Transport promise resolutions
// 1. load data
const data = await loadData(request);
// 2. render app
const appHtml = toString(<App data={data} />);
// 3. separate critical & deferred data
const criticalData = {};
const deferredData = {};
const deferredSetupScripts = [
// function used to setup deferred promises
`
window.__deferred = {};
function setupDeferredPromise(key) {
const promise = new Promise((resolve, reject) => {
window.__deferred[key] = {
resolve,
reject,
};
});
window.__data[key] = promise;
}
`,
// function used to resolve / reject deferred promises
`
function resolveDeferredPromise(key, reason, result) {
if (reason) {
window.__deferred[key].reject(reason);
} else {
window.__deferred[key].resolve(result);
}
}
`,
];
for (const [key, value] of Object.entries(data)) {
if (value && value instanceof Promise) {
deferredData[key] = value;
deferredSetupScripts.push(`
setupDeferredPromise(${JSON.stringify(key)});
`);
} else {
criticalData[key] = value;
}
}
response.send(
`
<!DOCTYPE>
<html>
<div id="app">
${appHtml}
</div>
<script>
// 4. serialize critical data
window.__data = ${JSON.stringify(criticalData)};
// 5. teleport promises
${deferredSetupScripts.join("\n")}
</script>
<script src="entry.client.js"></script>
</html>
`
);
// 5. transport promise resolutions
await Promise.allSettled(
Object.entries(deferredData).map(async ([key, promise]) => {
try {
const result = await promise;
response.send(
`
<script>
resolveDeferredPromise(
${JSON.stringify(key)},
undefined,
${JSON.stringify(result)}
);
</script>
`
);
} catch (reason) {
response.send(
`
<script>
const reason = new Error(${JSON.stringify(reason.message)});
${
reason.stack
? `reason.stack = ${JSON.stringify(reason.stack)}`
: ""
}
resolveDeferredPromise(
${JSON.stringify(key)},
reason,
);
</script>
`
);
}
})
);
response.end();