Streaming Dynamic and Asynchronous HTML with Hono and JSX

2 min read

Learn how to stream dynamic HTML content using Hono.js and JSX. This post provides a code example of how to use async generators and Suspense to stream non-append-only data to the browser, creating a dynamic and responsive user experience.

Hono is a web app framework for the latest generation of JavaScript and its new frameworks and runtimes. It's really great, and I like it a lot.

One of its really interesting features is that it supports streaming JSX to the browser, very similar to Next.js with React Server Components, but it does it with inline scripts without needing a full-scale framework in the browser.

You can even use Suspense and so on, allowing you to create, for example, an app shell with async logic inside:

<>
  <Header />
  <Suspense fallback={<Loading />}>
    <LoadAsyncData />
  </Suspense>
</>

But while I was experimenting with a recent project, I wanted to try streaming my results to the browser. Unfortunately, the data is not just append-only, so I wondered if there was a way to get Hono to replace the data.

After some experimentation and a lot of complicated attempts, I found a way to do it that turned out to be relatively simple. See the AsyncList component below for the solution:

async function AsyncList({
  generator,
}: {
  generator: AsyncGenerator<any, any, any>;
}) {
  const value = await generator.next();

  if (value.done) {
    return value.value;
  }

  return (
    <Suspense fallback={value.value}>
      <AsyncList generator={generator} />
    </Suspense>
  );
}

const timer = (ms: number) =>
  new Promise<string>((resolve) => setTimeout(() => resolve('hello!'), ms));

app.get('/test', (c) => {
  return c.render(
    <Suspense fallback={<div>Loading...</div>}>
      <AsyncList
        generator={(async function* () {
          yield <div>hello</div>;
          await timer(1000);
          yield <div>world</div>;
          await timer(1000);
          return <div>goodbye</div>;
        })()}
      />
    </Suspense>
  );
});