feat(react-renderer): Nested layout support for react-renderer (#449)
* feat: nested layout support for react-renderer * refactor * feat: describe changesetpull/457/head
parent
ffd67e5412
commit
284904357f
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@hono/react-renderer': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
nested layout support for react-renderer
|
|
@ -39,7 +39,7 @@
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18.2.17",
|
"@types/react-dom": "^18.2.17",
|
||||||
"esbuild": "^0.20.2",
|
"esbuild": "^0.20.2",
|
||||||
"hono": "^3.11.7",
|
"hono": "^4.2.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tsup": "^8.0.1",
|
"tsup": "^8.0.1",
|
||||||
|
|
|
@ -15,14 +15,21 @@ type BaseProps = {
|
||||||
children: React.ReactElement
|
children: React.ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ComponentProps = Props &
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
BaseProps & { Layout: React.FC<Record<string, any> & { children: React.ReactElement }> }
|
||||||
|
|
||||||
const RequestContext = React.createContext<Context | null>(null)
|
const RequestContext = React.createContext<Context | null>(null)
|
||||||
|
|
||||||
const createRenderer =
|
const createRenderer =
|
||||||
(c: Context, component?: React.FC<Props & BaseProps>, options?: RendererOptions) =>
|
(
|
||||||
|
c: Context,
|
||||||
|
Layout: React.FC<{ children: React.ReactElement }>,
|
||||||
|
component?: React.FC<ComponentProps>,
|
||||||
|
options?: RendererOptions
|
||||||
|
) =>
|
||||||
async (children: React.ReactElement, props?: Props) => {
|
async (children: React.ReactElement, props?: Props) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
const node = component ? component({ children, Layout, c, ...props }) : children
|
||||||
// @ts-ignore
|
|
||||||
const node = component ? component({ children, c, ...props }) : children
|
|
||||||
|
|
||||||
if (options?.stream) {
|
if (options?.stream) {
|
||||||
const { renderToReadableStream } = await import('react-dom/server')
|
const { renderToReadableStream } = await import('react-dom/server')
|
||||||
|
@ -53,11 +60,20 @@ const createRenderer =
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reactRenderer = (
|
export const reactRenderer = (
|
||||||
component?: React.FC<Props & BaseProps>,
|
component?: React.FC<ComponentProps>,
|
||||||
options?: RendererOptions
|
options?: RendererOptions
|
||||||
): MiddlewareHandler =>
|
): MiddlewareHandler =>
|
||||||
function reactRenderer(c, next) {
|
function reactRenderer(c, next) {
|
||||||
c.setRenderer(createRenderer(c, component, options))
|
const Layout = (c.getLayout() ?? React.Fragment) as React.FC<{
|
||||||
|
children: React.ReactElement
|
||||||
|
}>
|
||||||
|
if (component) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
c.setLayout((props: any) => {
|
||||||
|
return component({ ...props, Layout, c }, c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
c.setRenderer(createRenderer(c, Layout, component, options))
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,69 @@ describe('Basic', () => {
|
||||||
expect(await res.text()).toBe('<h1>http://localhost/</h1>')
|
expect(await res.text()).toBe('<h1>http://localhost/</h1>')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('nested layout with Layout', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
app.use(
|
||||||
|
'*',
|
||||||
|
// @ts-expect-error - `title` is not defined
|
||||||
|
reactRenderer(({ children, title, Layout }) => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<html>
|
||||||
|
<head>{title}</head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const app2 = new Hono()
|
||||||
|
app2.use(
|
||||||
|
'*',
|
||||||
|
// @ts-expect-error - `title` is not defined
|
||||||
|
reactRenderer(({ children, Layout, title }) => {
|
||||||
|
return (
|
||||||
|
<Layout title={title}>
|
||||||
|
<div className='nested'>{children}</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app2.get('/', (c) => c.render(<h1>http://localhost/nested</h1>, { title: 'Nested' }))
|
||||||
|
|
||||||
|
const app3 = new Hono()
|
||||||
|
app3.use(
|
||||||
|
'*',
|
||||||
|
// @ts-expect-error - `title` is not defined
|
||||||
|
reactRenderer(({ children, Layout, title }) => {
|
||||||
|
return (
|
||||||
|
<Layout title={title}>
|
||||||
|
<div className='nested2'>{children}</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app3.get('/', (c) => c.render(<h1>http://localhost/nested</h1>, { title: 'Nested2' }))
|
||||||
|
app2.route('/nested2', app3)
|
||||||
|
|
||||||
|
app.route('/nested', app2)
|
||||||
|
|
||||||
|
let res = await app.request('http://localhost/nested')
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe(
|
||||||
|
'<html><head>Nested</head><body><div class="nested"><h1>http://localhost/nested</h1></div></body></html>'
|
||||||
|
)
|
||||||
|
|
||||||
|
res = await app.request('http://localhost/nested/nested2')
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe(
|
||||||
|
'<html><head>Nested2</head><body><div class="nested"><div class="nested2"><h1>http://localhost/nested</h1></div></div></body></html>'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('Should return a default doctype', async () => {
|
it('Should return a default doctype', async () => {
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
app.use(
|
app.use(
|
||||||
|
|
|
@ -2013,7 +2013,7 @@ __metadata:
|
||||||
"@types/react": "npm:^18"
|
"@types/react": "npm:^18"
|
||||||
"@types/react-dom": "npm:^18.2.17"
|
"@types/react-dom": "npm:^18.2.17"
|
||||||
esbuild: "npm:^0.20.2"
|
esbuild: "npm:^0.20.2"
|
||||||
hono: "npm:^3.11.7"
|
hono: "npm:^4.2.3"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
react-dom: "npm:^18.2.0"
|
react-dom: "npm:^18.2.0"
|
||||||
tsup: "npm:^8.0.1"
|
tsup: "npm:^8.0.1"
|
||||||
|
@ -9661,6 +9661,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"hono@npm:^4.2.3":
|
||||||
|
version: 4.2.3
|
||||||
|
resolution: "hono@npm:4.2.3"
|
||||||
|
checksum: eced562fcfc97cd4f2fd4fb081e99775d23c58c8fb1a6c57311ddfba71c23e1da4d33294b0ab1fdd0c4587351dc278f4f1f7d02fcd0ded156c32c74e66a01f45
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"hosted-git-info@npm:^2.1.4":
|
"hosted-git-info@npm:^2.1.4":
|
||||||
version: 2.8.9
|
version: 2.8.9
|
||||||
resolution: "hosted-git-info@npm:2.8.9"
|
resolution: "hosted-git-info@npm:2.8.9"
|
||||||
|
|
Loading…
Reference in New Issue