Let's create an application that includes a client side. Here, we will use hono/jsx/dom.
Below is the project structure of a minimal application including a client side:
.
├── app
│ ├── client.ts // client entry file
│ ├── global.d.ts
│ ├── islands
│ │ └── counter.tsx // island component
│ ├── routes
│ │ ├── _renderer.tsx
│ │ └── index.tsx
│ └── server.ts
├── package.json
├── tsconfig.json
└── vite.config.ts
This is a _renderer.tsx
, which will load the /app/client.ts
entry file for the client. It will load the JavaScript file for production according to the variable import.meta.env.PROD
. And renders the inside of <HasIslands />
if there are islands on that page.
import { const jsxRenderer: (component?: ComponentWithChildren, options?: RendererOptions) => MiddlewareHandler
JSX Renderer Middleware for hono.jsxRenderer } from 'hono/jsx-renderer'
import { const HasIslands: ({ children }: {
children: any;
}) => any
HasIslands } from 'honox/server'
export default function jsxRenderer(component?: ComponentWithChildren, options?: RendererOptions): MiddlewareHandler
JSX Renderer Middleware for hono.jsxRenderer(({ children: Child
children }) => {
return (
<JSX.IntrinsicElements.html: HtmlHTMLAttributes
html JSX.HTMLAttributes.lang?: string | undefined
lang='en'>
<JSX.IntrinsicElements.head: JSX.HTMLAttributes
head>
<JSX.IntrinsicElements.meta: MetaHTMLAttributes
meta MetaHTMLAttributes.charset?: StringLiteralUnion<"utf-8"> | undefined
charset='UTF-8' />
<JSX.IntrinsicElements.meta: MetaHTMLAttributes
meta MetaHTMLAttributes.name?: StringLiteralUnion<MetaName> | undefined
name='viewport' MetaHTMLAttributes.content?: string | undefined
content='width=device-width, initial-scale=1.0' />
{import.meta.ImportMeta.env: ImportMetaEnv
env.ImportMetaEnv.PROD: boolean
PROD ? (
<const HasIslands: ({ children }: {
children: any;
}) => any
HasIslands>
<JSX.IntrinsicElements.script: ScriptHTMLAttributes
script ScriptHTMLAttributes.type?: StringLiteralUnion<"" | "module" | "text/javascript" | "importmap"> | undefined
type='module' ScriptHTMLAttributes.src?: string | undefined
src='/static/client.js'></JSX.IntrinsicElements.script: ScriptHTMLAttributes
script>
</const HasIslands: ({ children }: {
children: any;
}) => any
HasIslands>
) : (
<JSX.IntrinsicElements.script: ScriptHTMLAttributes
script ScriptHTMLAttributes.type?: StringLiteralUnion<"" | "module" | "text/javascript" | "importmap"> | undefined
type='module' ScriptHTMLAttributes.src?: string | undefined
src='/app/client.ts'></JSX.IntrinsicElements.script: ScriptHTMLAttributes
script>
)}
</JSX.IntrinsicElements.head: JSX.HTMLAttributes
head>
<JSX.IntrinsicElements.body: JSX.HTMLAttributes
body>{children: Child
children}</JSX.IntrinsicElements.body: JSX.HTMLAttributes
body>
</JSX.IntrinsicElements.html: HtmlHTMLAttributes
html>
)
})
If you have a manifest file in dist/.vite/manifest.json
, you can easily write it using <Script />
.
import { const jsxRenderer: (component?: ComponentWithChildren, options?: RendererOptions) => MiddlewareHandler
JSX Renderer Middleware for hono.jsxRenderer } from 'hono/jsx-renderer'
import { const Script: (options: Options) => any
Script } from 'honox/server'
export default function jsxRenderer(component?: ComponentWithChildren, options?: RendererOptions): MiddlewareHandler
JSX Renderer Middleware for hono.jsxRenderer(({ children: Child
children }) => {
return (
<JSX.IntrinsicElements.html: HtmlHTMLAttributes
html JSX.HTMLAttributes.lang?: string | undefined
lang='en'>
<JSX.IntrinsicElements.head: JSX.HTMLAttributes
head>
<JSX.IntrinsicElements.meta: MetaHTMLAttributes
meta MetaHTMLAttributes.charset?: StringLiteralUnion<"utf-8"> | undefined
charset='UTF-8' />
<JSX.IntrinsicElements.meta: MetaHTMLAttributes
meta MetaHTMLAttributes.name?: StringLiteralUnion<MetaName> | undefined
name='viewport' MetaHTMLAttributes.content?: string | undefined
content='width=device-width, initial-scale=1.0' />
<const Script: (options: Options) => any
Script src: string
src='/app/client.ts' />
</JSX.IntrinsicElements.head: JSX.HTMLAttributes
head>
<JSX.IntrinsicElements.body: JSX.HTMLAttributes
body>{children: Child
children}</JSX.IntrinsicElements.body: JSX.HTMLAttributes
body>
</JSX.IntrinsicElements.html: HtmlHTMLAttributes
html>
)
})
Note: Since <HasIslands />
can slightly affect build performance when used, it is recommended that you do not use it in the development environment, but only at build time. <Script />
does not cause performance degradation during development, so it's better to use it.
If you want to add a nonce
attribute to <Script />
or <script />
element, you can use Security Headers Middleware.
Define the middleware:
// @filename: app/routes/_middleware.ts
import { const createRoute: CreateHandlersInterface<Env, any>
createRoute } from 'honox/factory'
import { const secureHeaders: (customOptions?: SecureHeadersOptions) => MiddlewareHandler
Secure Headers Middleware for Hono.secureHeaders, const NONCE: ContentSecurityPolicyOptionHandler
NONCE } from 'hono/secure-headers'
function secureHeaders(customOptions?: SecureHeadersOptions): MiddlewareHandler
Secure Headers Middleware for Hono.secureHeaders({
SecureHeadersOptions.contentSecurityPolicy?: ContentSecurityPolicyOptions | undefined
contentSecurityPolicy: {
ContentSecurityPolicyOptions.scriptSrc?: ContentSecurityPolicyOptionValue | undefined
scriptSrc: [const NONCE: ContentSecurityPolicyOptionHandler
NONCE],
},
})
You can get the nonce
value with c.get('secureHeadersNonce')
:
import { const jsxRenderer: (component?: ComponentWithChildren, options?: RendererOptions) => MiddlewareHandler
JSX Renderer Middleware for hono.jsxRenderer } from 'hono/jsx-renderer'
import { const Script: (options: Options) => any
Script } from 'honox/server'
export default function jsxRenderer(component?: ComponentWithChildren, options?: RendererOptions): MiddlewareHandler
JSX Renderer Middleware for hono.jsxRenderer(({ children: Child
children }, c: Context<any, any, {}>
c) => {
return (
<JSX.IntrinsicElements.html: HtmlHTMLAttributes
html JSX.HTMLAttributes.lang?: string | undefined
lang='en'>
<JSX.IntrinsicElements.head: JSX.HTMLAttributes
head>
<const Script: (options: Options) => any
Script src: string
src='/app/client.ts' async?: boolean | undefined
async nonce?: string | undefined
nonce={c: Context<any, any, {}>
c.Context<any, any, {}>.get: Get
<"secureHeadersNonce">(key: "secureHeadersNonce") => any (+1 overload)
get('secureHeadersNonce')} />
</JSX.IntrinsicElements.head: JSX.HTMLAttributes
head>
<JSX.IntrinsicElements.body: JSX.HTMLAttributes
body>{children: Child
children}</JSX.IntrinsicElements.body: JSX.HTMLAttributes
body>
</JSX.IntrinsicElements.html: HtmlHTMLAttributes
html>
)
})
A client-side entry file should be in app/client.ts
. Simply, write createClient()
.
// @filename: app/client.ts
import { const createClient: (options?: ClientOptions) => Promise<void>
createClient } from 'honox/client'
function createClient(options?: ClientOptions): Promise<void>
createClient()
If you want to add interactions to your page, create Island components. Islands components should be:
app/islands
directory or named with $
prefix like $componentName.tsx
.default
or a proper component name that uses camel case but does not contain _
and is not all uppercase.For example, you can write an interactive component such as the following counter:
import { const useState: UseStateType
useState } from 'hono/jsx'
export default function function Counter(): JSX.Element
Counter() {
const [const count: number
count, const setCount: UpdateStateFunction<number>
setCount] = useState<number>(initialState: number | (() => number)): [number, UpdateStateFunction<number>] (+1 overload)
useState(0)
return (
<JSX.IntrinsicElements.div: JSX.HTMLAttributes
div>
<JSX.IntrinsicElements.p: JSX.HTMLAttributes
p>Count: {const count: number
count}</JSX.IntrinsicElements.p: JSX.HTMLAttributes
p>
<JSX.IntrinsicElements.button: ButtonHTMLAttributes
button EventAttributes.onClick?: ((event: MouseEvent) => void) | undefined
onClick={() => const setCount: (newState: number | ((currentState: number) => number)) => void
setCount(const count: number
count + 1)}>Increment</JSX.IntrinsicElements.button: ButtonHTMLAttributes
button>
</JSX.IntrinsicElements.div: JSX.HTMLAttributes
div>
)
}
// When you load the component in a route file, it is rendered as Server-Side rendering and JavaScript is also sent to the client side.
// @filename: app/routes/index.tsx
/** @jsx jsx */
/** @jsxImportSource hono/jsx */
import { const createRoute: CreateHandlersInterface<Env, any>
createRoute } from 'honox/factory'
import function Counter(): JSX.Element
Counter from '../islands/counter'
export default createRoute<{}, Response | Promise<Response>, Env>(handler1: H<Env, any, {}, Response | Promise<Response>>): [...] (+9 overloads)
createRoute((c: Context<Env, any, {}>
c) => {
return c: Context<Env, any, {}>
c.Context<Env, any, {}>.render: DefaultRenderer
(content: string | Promise<string>) => Response | Promise<Response>
`.render()` can create a response within a layout.render(
<JSX.IntrinsicElements.div: JSX.HTMLAttributes
div>
<JSX.IntrinsicElements.h1: JSX.HTMLAttributes
h1>Hello</JSX.IntrinsicElements.h1: JSX.HTMLAttributes
h1>
<function Counter(): JSX.Element
Counter />
</JSX.IntrinsicElements.div: JSX.HTMLAttributes
div>
)
})
Note: You cannot access a Context object in Island components. Therefore, you should pass the value from components outside of the Island.
import { const useRequestContext: <E extends Env = any, P extends string = any, I extends Input = {}>() => Context<E, P, I>
useRequestContext for Hono.useRequestContext } from 'hono/jsx-renderer'
import function Counter({ init }: {
init: number;
}): JSX.Element
Counter from '../islands/counter'
export default function function Component(): JSX.Element
Component() {
const const c: Context<any, any, {}>
c = useRequestContext<any, any, {}>(): Context<any, any, {}>
useRequestContext for Hono.useRequestContext()
return <function Counter({ init }: {
init: number;
}): JSX.Element
Counter init: number
init={function parseInt(string: string, radix?: number): number
Converts a string to an integer.parseInt(const c: Context<any, any, {}>
c.Context<any, any, {}>.req: HonoRequest<any, unknown>
`.req` is the instance of
{@link
HonoRequest
}
.req.HonoRequest<any, unknown>.query(key: string): string | undefined (+1 overload)
`.query()` can get querystring parameters.query('count') ?? '0', 10)} />
}