honojs-middleware/packages/react-renderer/src/react-renderer.ts

69 lines
2.0 KiB
TypeScript
Raw Normal View History

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
}