-
-
Notifications
You must be signed in to change notification settings - Fork 545
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Introduce streaming API with Suspense
and use
.
#1630
Conversation
09827d0
to
1cda785
Compare
1cda785
to
ec17ac9
Compare
Refactoring is complete. |
0ceee27
to
d25703d
Compare
6fcbcce
to
582a6fa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update package.json
for hono/jsx/streaming
:
"exports": {
//...
"./jsx/streaming": {
"types": "./dist/types/jsx/streaming.d.ts",
"import": "./dist/jsx/streaming.js",
"require": "./dist/cjs/jsx/streaming.js"
},
"typesVersions": {
"*": {
//...
"jsx/streaming": [
"./dist/types/jsx/streaming.d.ts"
],
Hi @usualoma! Sorry for the late reply. I've tested this feature thoroughly and it's amazing! The example below demonstrates it working seamlessly: import { Hono } from 'hono'
import { use, Suspense, renderToReadableStream } from 'hono/jsx/streaming'
import type { Shop } from './types'
const app = new Hono()
const fetchData = async (): Promise<{ shop: Shop }> => {
const res = await fetch('https://ramen-api.dev/shops/yoshimuraya')
return res.json()
}
const AsyncComponent = () => {
const data = use(fetchData())
return <p>I like {data.shop.name} 🍜</p>
}
app.get('/', async (c) => {
const stream = renderToReadableStream(
<html>
<body>
<h1>SSR Streaming</h1>
<Suspense fallback={<p>loading...</p>}>
<AsyncComponent />
</Suspense>
</body>
</html>
)
return c.body(stream, {
headers: {
'Transfer-Encoding': 'chunked',
'Content-Type': 'text/html; charset=UTF-8'
}
})
})
export default app Additionally, with this PR honojs/vite-plugins#19, the I've left some comments on the PR, please check them out. I believe the streaming methods: |
867121c
to
ec12034
Compare
Hi @yusukebe! b91a87dBasically, exceptions should be caught by the application and not by the framework, but if we don't catch them, streaming will not be closed, so we catch them. I think the test is OK, but when I add this test, the vitest catches the global unhandledRejection and makes an error, so I now skip it. Demo appYour demo app is simple and great. It works well. However, the current code calls fetch() needlessly, so I think it would be better to change it a little. (Sorry if you knew what you were doing...) As is the case with React, the following code will display "fetchData" multiple times. import React, { Suspense, use } from "react";
import { renderToReadableStream } from "react-dom/server";
const fetchData = async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
console.log("fetchData");
return "OK";
};
function Component() {
const res = use(fetchData());
return <p>${res}</p>;
}
const elm = (
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
);
const stream = await renderToReadableStream(elm);
const decoder = new TextDecoder("utf-8");
for await (const chunk of stream) {
console.log(decoder.decode(chunk));
} For example, if the result of fetch() is cached as a Promise as shown below, unnecessary fetch() will not be called. import { Hono } from 'hono'
import { use, Suspense, renderToReadableStream } from 'hono/jsx/streaming'
import type { Shop } from './types'
const app = new Hono()
const fetchDataCache = {}
const fetchData = (name): Promise<{ shop: Shop }> =>
(fetchDataCache[name] ||= fetch(`https://ramen-api.dev/shops/${name}`).then((res) => res.json()))
const AsyncComponent = () => {
const data = use(fetchData("yoshimuraya"))
return <p>I like {data.shop.name} 🍜</p>
}
app.get('/', async (c) => {
const stream = renderToReadableStream(
<html>
<body>
<h1>SSR Streaming</h1>
<Suspense fallback={<p>loading...</p>}>
<AsyncComponent />
</Suspense>
</body>
</html>
)
return c.body(stream, {
headers: {
'Transfer-Encoding': 'chunked',
'Content-Type': 'text/html; charset=UTF-8',
},
})
})
export default app |
Hi @usualoma, Thanks. I understood both. Additionally, something awesome happened last night. One of the React members, Dan, mentioned the honojs account on Twitter and gave us an advice. It's an honor to receive an advice from a React committer regarding JSX. https://x.com/dan_abramov/status/1721179995370914192?s=20 I agree with what he said; the const AsyncComponent = async () => {
const res = await fetch(`https://ramen-api.dev/shops/yoshimuraya`)
const data = await res.json()
return <p>I like {data.shop.name} 🍜</p>
}
app.get('/', async (c) => {
const stream = renderToReadableStream(
<html>
<body>
<h1>SSR Streaming</h1>
<Suspense fallback={<p>loading...</p>}>
<AsyncComponent />
</Suspense>
</body>
</html>
)
// ...
}) This is enabled if the Async Component inside What do you think about this? If we can implement it, I believe we won't need to create |
Hi @yusukebe! Oh, that is a great honor and much-appreciated advice! I implemented that in f4589b7. |
Thanks!
He might have been referring only to the hono/jsx spec, and not React, as he knows our hono/jsx doesn't follow the React spec completely. In React, it's still necessary to throw a Promise in Suspense, and maybe it should support client components too (although I don't have took a look the spec well). Anyway. All things are done. I'll prepare the "next" branch and I'll merge this into it. |
Uh, I wrote the code to make sure it worked. (I haven't read the code for React's internal implementation though.) The following code using React's Suspense is handled as streaming content in SSR. import { Hono } from "hono";
import React, { Suspense } from "react"; // 18.3.0
import { renderToReadableStream } from "react-dom/server";
const app = new Hono();
async function Component() {
const data = await (
await fetch("https://ramen-api.dev/shops/takasagoya")
).json();
await new Promise((resolve) => setTimeout(resolve, 1000));
return <h1>{data.shop.name}</h1>;
}
app.get("/", async (c) => {
const stream = renderToReadableStream(
<html>
<head>
<meta charSet="utf-8" />
</head>
<body>
<Suspense fallback={<div>Loading...</div>}>
<Component />
</Suspense>
</body>
</html>
);
return c.body(await stream, 200, {
"X-Content-Type-Options": "nosniff",
"Content-Type": "text/html",
});
});
export default app; ssrstreaming.movTherefore, the fact that async components are treated as streaming content in Suspense is not a specification unique to hono, but a behavior compatible with the original React. |
I got it! As you said, this is good thing for React user to migrate to hono/jsx. Great! |
It's a time for merging! I'll merge this into the "next" branch and release a RC version first. Thanks! |
@yusukebe I'm testing this out and having some TypeScript trouble with the latest example above. I've got "jsx": "react",
"jsxFactory": "jsx",
"jsxFragmentFactory": "Fragment" in my tsconfig.json file, but JSX components aren't being recognized. Any suggestions? Update: I am a silly person and did not use a |
This PR is based on #1626.
Author should do the followings, if applicable
yarn denoify
to generate files for Deno