-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce React Renderer Middleware (#309)
* feat: introduce React Renderer Middleware * docs: update readme * chore: add changeset * use `global.d.ts` * remove global.d.ts * import types correctly
- Loading branch information
Showing
9 changed files
with
544 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@hono/react-renderer': patch | ||
--- | ||
|
||
feat: introduce React Renderer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
# React Renderer Middleware | ||
|
||
React Renderer Middleware allows for the easy creation of a renderer based on React for Hono. | ||
|
||
## Installation | ||
|
||
```txt | ||
npm i @hono/react-renderer react react-dom hono | ||
npm i -D @types/react @types/react-dom | ||
``` | ||
|
||
## Settings | ||
|
||
`tsconfig.json`: | ||
|
||
```json | ||
{ | ||
"compilerOptions": { | ||
"jsx": "react-jsx", | ||
"jsxImportSource": "react" | ||
} | ||
} | ||
``` | ||
|
||
## Usage | ||
|
||
### Basic | ||
|
||
```tsx | ||
import { Hono } from 'hono' | ||
import { reactRenderer } from '@hono/react-renderer' | ||
|
||
const app = new Hono() | ||
|
||
app.get( | ||
'*', | ||
reactRenderer(({ children }) => { | ||
return ( | ||
<html> | ||
<body> | ||
<h1>React + Hono</h1> | ||
<div>{children}</div> | ||
</body> | ||
</html> | ||
) | ||
}) | ||
) | ||
|
||
app.get('/', (c) => { | ||
return c.render(<p>Welcome!</p>) | ||
}) | ||
``` | ||
|
||
### Extending `Props` | ||
|
||
You can define types of `Props`: | ||
|
||
```tsx | ||
declare module '@hono/react-renderer' { | ||
interface Props { | ||
title: string | ||
} | ||
} | ||
``` | ||
|
||
Then, you can use it in the `reactRenderer()` function and pass values as a second argument to `c.render()`: | ||
|
||
```tsx | ||
app.get( | ||
'*', | ||
reactRenderer(({ children, title }) => { | ||
return ( | ||
<html> | ||
<head> | ||
<title>{title}</title> | ||
</head> | ||
<body> | ||
<div>{children}</div> | ||
</body> | ||
</html> | ||
) | ||
}) | ||
) | ||
|
||
app.get('/', (c) => { | ||
return c.render(<p>Welcome!</p>, { | ||
title: 'Top Page', | ||
}) | ||
}) | ||
``` | ||
|
||
### `useRequestContext()` | ||
|
||
You can get an instance of `Context` in a function component: | ||
|
||
```tsx | ||
const Component = () => { | ||
const c = useRequestContext() | ||
return <p>You access {c.req.url}</p> | ||
} | ||
|
||
app.get('/', (c) => { | ||
return c.render(<Component />) | ||
}) | ||
``` | ||
|
||
## Options | ||
|
||
### `docType` | ||
|
||
If you set it `true`, `DOCTYPE` will be added: | ||
|
||
```tsx | ||
app.get( | ||
'*', | ||
reactRenderer( | ||
({ children }) => { | ||
return ( | ||
<html> | ||
<body> | ||
<div>{children}</div> | ||
</body> | ||
</html> | ||
) | ||
}, | ||
{ | ||
docType: true, | ||
} | ||
) | ||
) | ||
``` | ||
|
||
The HTML is the following: | ||
|
||
```html | ||
<!DOCTYPE html> | ||
<html> | ||
<body> | ||
<div><p>Welcome!</p></div> | ||
</body> | ||
</html> | ||
``` | ||
|
||
You can specify the `docType` as you like. | ||
|
||
```tsx | ||
app.get( | ||
'*', | ||
reactRenderer( | ||
({ children }) => { | ||
return ( | ||
<html> | ||
<body> | ||
<div>{children}</div> | ||
</body> | ||
</html> | ||
) | ||
}, | ||
{ | ||
docType: | ||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', | ||
} | ||
) | ||
) | ||
``` | ||
|
||
### `stream` | ||
|
||
It enables returning a streaming response. You can use a `Suspense` with it: | ||
|
||
```tsx | ||
import { reactRenderer } from '@hono/react-renderer' | ||
import { Suspense } from 'react' | ||
|
||
app.get( | ||
'*', | ||
reactRenderer( | ||
({ children }) => { | ||
return ( | ||
<html> | ||
<body> | ||
<div>{children}</div> | ||
</body> | ||
</html> | ||
) | ||
}, | ||
{ | ||
stream: true, | ||
} | ||
) | ||
) | ||
|
||
let done = false | ||
|
||
const Component = () => { | ||
if (done) { | ||
return <p>Done!</p> | ||
} | ||
throw new Promise((resolve) => { | ||
done = true | ||
setTimeout(resolve, 1000) | ||
}) | ||
} | ||
|
||
app.get('/', (c) => { | ||
return c.render( | ||
<Suspense fallback='loading...'> | ||
<Component /> | ||
</Suspense> | ||
) | ||
}) | ||
``` | ||
|
||
## Limitation | ||
|
||
A streaming feature is not available on Vite or Vitest. | ||
|
||
## Author | ||
|
||
Yusuke Wada <https://github.com/yusukebe> | ||
|
||
## License | ||
|
||
MIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{ | ||
"name": "@hono/react-renderer", | ||
"version": "0.0.0", | ||
"description": "React Renderer Middleware for Hono", | ||
"type": "module", | ||
"main": "dist/index.js", | ||
"module": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"test": "vitest --run", | ||
"build": "tsup ./src/index.ts --external hono,react,react-dom --format esm,cjs --dts", | ||
"publint": "publint" | ||
}, | ||
"exports": { | ||
".": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.js", | ||
"require": "./dist/index.cjs" | ||
} | ||
}, | ||
"license": "MIT", | ||
"publishConfig": { | ||
"registry": "https://registry.npmjs.org", | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/honojs/middleware.git" | ||
}, | ||
"homepage": "https://github.com/honojs/middleware", | ||
"peerDependencies": { | ||
"hono": "*" | ||
}, | ||
"devDependencies": { | ||
"@types/react": "^18", | ||
"@types/react-dom": "^18.2.17", | ||
"hono": "^3.11.7", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"tsup": "^8.0.1", | ||
"vitest": "^1.0.4" | ||
}, | ||
"engines": { | ||
"node": ">=18" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export * from './react-renderer' | ||
|
||
export type Props = {} | ||
|
||
declare module 'hono' { | ||
interface ContextRenderer { | ||
(children: React.ReactElement, props?: Props): Response | Promise<Response> | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import type { Context } from 'hono' | ||
import type { Env, MiddlewareHandler } from 'hono/types' | ||
import React from 'react' | ||
import { renderToString, renderToReadableStream } from 'react-dom/server' | ||
import type { Props } from '.' | ||
|
||
type RendererOptions = { | ||
docType?: boolean | string | ||
stream?: boolean | Record<string, string> | ||
} | ||
|
||
type BaseProps = { | ||
c: Context | ||
children: React.ReactElement | ||
} | ||
|
||
const RequestContext = React.createContext<Context | null>(null) | ||
|
||
const createRenderer = | ||
(c: Context, component?: React.FC<Props & BaseProps>, options?: RendererOptions) => | ||
async (children: React.ReactElement, props?: Props) => { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
const node = component ? component({ children, c, ...props }) : children | ||
|
||
if (options?.stream) { | ||
const stream = await renderToReadableStream( | ||
React.createElement(RequestContext.Provider, { value: c }, node) | ||
) | ||
return c.body(stream, { | ||
headers: | ||
options.stream === true | ||
? { | ||
'Transfer-Encoding': 'chunked', | ||
'Content-Type': 'text/html; charset=UTF-8', | ||
} | ||
: options.stream, | ||
}) | ||
} else { | ||
const docType = | ||
typeof options?.docType === 'string' | ||
? options.docType | ||
: options?.docType === true | ||
? '<!DOCTYPE html>' | ||
: '' | ||
const body = | ||
docType + renderToString(React.createElement(RequestContext.Provider, { value: c }, node)) | ||
return c.html(body) | ||
} | ||
} | ||
|
||
export const reactRenderer = ( | ||
component?: React.FC<Props & BaseProps>, | ||
options?: RendererOptions | ||
): MiddlewareHandler => | ||
function reactRenderer(c, next) { | ||
c.setRenderer(createRenderer(c, component, options)) | ||
return next() | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export const useRequestContext = <E extends Env = any>(): Context<E> => { | ||
const c = React.useContext(RequestContext) | ||
if (!c) { | ||
throw new Error('RequestContext is not provided.') | ||
} | ||
return c | ||
} |
Oops, something went wrong.