React Suspense for static site generation
In 18.2 of React there is an addition of Suspense. This allows an interface for handling async data sources and interactions for smoother interface management and also handling concurrent rendering.
For static site generation we can use these new suspense functionality to process data and create static HTML pages. For my usage I want to use React as a templating language. Then we can use data (ex. JSON) to populate our template.
My initial approach was full of issues due to my knowledge of how Suspense works. Suspense works on async data but you need to wrap it in a sync function. The function will be executed until it completes without throwing an error.
Wrap promise
function wrapPromise(promise) {
// Keep track of the status of the result
let status = "pending";
// We keep track of the result of the promise here
let result;
// activate the promise to wait for the response
const suspense = promise.then(
(r) => {
status = "ok";
result = r;
},
(e) => {
status = "error";
result = e;
},
);
// Returning an object here allows us to access the variables in this function
return {
read() {
if (status == "pending") {
// we are not complete, throw the promise again
throw suspense;
} else if (status == "ok") {
return result;
} else {
throw e;
}
},
};
}
// We wrap our promise in our sync function
const fetchPhotos = wrapPromise(
fetch("https://api.flickr.com/photos/rockerBOO")
.then((resp) => resp.json())
.then((results) => results.photos.photo),
);
Suspense components
File: photos.jsx
function Photos() {
const photos = fetchPhotos.read();
return <div>
{photos.map(photo => {
return <div>{photo.title}</div>
}}
</div>
}
export default Photos
File: app.jsx
function App() {
return (
<html>
<head>
<meta charset="utf-8" />
<title>Title</title>
</head>
<body>
<Suspense fallback={<div>loading...</div>}>
<Photos />
</Suspense>
</body>
</html>
);
}
export default App;
A few different configurations for how these pieces could work together. We are not making a client version of React here we can make some concessions of how we organize it. If you are doing a hybrid solution, you may need to do other things.
Render to readable stream
In this example, I’m using bun but only small changes to make it work for node.js, Deno, Cloudflare workers or other runtimes.
const stream = await renderToReadableStream(<App />, {
onError(error) {
console.error(error);
},
});
// Wait for all the suspense data sources to complete
await stream.allReady;
// Convert the stream to text (using the Bun functionality)
const html = await Bun.readableStreamToText(stream);
Then you can use the HTML to save to a file or serve from the readable stream you want a dynamic interface.