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) => MiddlewareHandlerJSX 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): MiddlewareHandlerJSX Renderer Middleware for hono.jsxRenderer(({ children: Childchildren }) => {
return (
<JSX.IntrinsicElements.html: HtmlHTMLAttributeshtml JSX.HTMLAttributes.lang?: string | undefinedlang='en'>
<JSX.IntrinsicElements.head: JSX.HTMLAttributeshead>
<JSX.IntrinsicElements.meta: MetaHTMLAttributesmeta MetaHTMLAttributes.charset?: StringLiteralUnion<"utf-8"> | undefinedcharset='UTF-8' />
<JSX.IntrinsicElements.meta: MetaHTMLAttributesmeta MetaHTMLAttributes.name?: StringLiteralUnion<MetaName> | undefinedname='viewport' MetaHTMLAttributes.content?: string | undefinedcontent='width=device-width, initial-scale=1.0' />
{import.meta.ImportMeta.env: ImportMetaEnvenv.ImportMetaEnv.PROD: booleanPROD ? (
<const HasIslands: ({ children }: {
children: any;
}) => any
HasIslands>
<JSX.IntrinsicElements.script: ScriptHTMLAttributesscript ScriptHTMLAttributes.type?: StringLiteralUnion<"" | "module" | "text/javascript" | "importmap"> | undefinedtype='module' ScriptHTMLAttributes.src?: string | undefinedsrc='/static/client.js'></JSX.IntrinsicElements.script: ScriptHTMLAttributesscript>
</const HasIslands: ({ children }: {
children: any;
}) => any
HasIslands>
) : (
<JSX.IntrinsicElements.script: ScriptHTMLAttributesscript ScriptHTMLAttributes.type?: StringLiteralUnion<"" | "module" | "text/javascript" | "importmap"> | undefinedtype='module' ScriptHTMLAttributes.src?: string | undefinedsrc='/app/client.ts'></JSX.IntrinsicElements.script: ScriptHTMLAttributesscript>
)}
</JSX.IntrinsicElements.head: JSX.HTMLAttributeshead>
<JSX.IntrinsicElements.body: JSX.HTMLAttributesbody>{children: Childchildren}</JSX.IntrinsicElements.body: JSX.HTMLAttributesbody>
</JSX.IntrinsicElements.html: HtmlHTMLAttributeshtml>
)
})
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) => MiddlewareHandlerJSX Renderer Middleware for hono.jsxRenderer } from 'hono/jsx-renderer'
import { const Script: (options: Options) => anyScript } from 'honox/server'
export default function jsxRenderer(component?: ComponentWithChildren, options?: RendererOptions): MiddlewareHandlerJSX Renderer Middleware for hono.jsxRenderer(({ children: Childchildren }) => {
return (
<JSX.IntrinsicElements.html: HtmlHTMLAttributeshtml JSX.HTMLAttributes.lang?: string | undefinedlang='en'>
<JSX.IntrinsicElements.head: JSX.HTMLAttributeshead>
<JSX.IntrinsicElements.meta: MetaHTMLAttributesmeta MetaHTMLAttributes.charset?: StringLiteralUnion<"utf-8"> | undefinedcharset='UTF-8' />
<JSX.IntrinsicElements.meta: MetaHTMLAttributesmeta MetaHTMLAttributes.name?: StringLiteralUnion<MetaName> | undefinedname='viewport' MetaHTMLAttributes.content?: string | undefinedcontent='width=device-width, initial-scale=1.0' />
<const Script: (options: Options) => anyScript src: stringsrc='/app/client.ts' />
</JSX.IntrinsicElements.head: JSX.HTMLAttributeshead>
<JSX.IntrinsicElements.body: JSX.HTMLAttributesbody>{children: Childchildren}</JSX.IntrinsicElements.body: JSX.HTMLAttributesbody>
</JSX.IntrinsicElements.html: HtmlHTMLAttributeshtml>
)
})
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) => MiddlewareHandlerSecure Headers Middleware for Hono.secureHeaders, const NONCE: ContentSecurityPolicyOptionHandlerNONCE } from 'hono/secure-headers'
function secureHeaders(customOptions?: SecureHeadersOptions): MiddlewareHandlerSecure Headers Middleware for Hono.secureHeaders({
SecureHeadersOptions.contentSecurityPolicy?: ContentSecurityPolicyOptions | undefinedcontentSecurityPolicy: {
ContentSecurityPolicyOptions.scriptSrc?: ContentSecurityPolicyOptionValue | undefinedscriptSrc: [const NONCE: ContentSecurityPolicyOptionHandlerNONCE],
},
})
You can get the nonce value with c.get('secureHeadersNonce'):
import { const jsxRenderer: (component?: ComponentWithChildren, options?: RendererOptions) => MiddlewareHandlerJSX Renderer Middleware for hono.jsxRenderer } from 'hono/jsx-renderer'
import { const Script: (options: Options) => anyScript } from 'honox/server'
export default function jsxRenderer(component?: ComponentWithChildren, options?: RendererOptions): MiddlewareHandlerJSX Renderer Middleware for hono.jsxRenderer(({ children: Childchildren }, c: Context<any, any, {}>c) => {
return (
<JSX.IntrinsicElements.html: HtmlHTMLAttributeshtml JSX.HTMLAttributes.lang?: string | undefinedlang='en'>
<JSX.IntrinsicElements.head: JSX.HTMLAttributeshead>
<const Script: (options: Options) => anyScript src: stringsrc='/app/client.ts' async?: boolean | undefinedasync nonce?: string | undefinednonce={c: Context<any, any, {}>c.Context<any, any, {}>.get: Get
<"secureHeadersNonce">(key: "secureHeadersNonce") => any (+1 overload)
get('secureHeadersNonce')} />
</JSX.IntrinsicElements.head: JSX.HTMLAttributeshead>
<JSX.IntrinsicElements.body: JSX.HTMLAttributesbody>{children: Childchildren}</JSX.IntrinsicElements.body: JSX.HTMLAttributesbody>
</JSX.IntrinsicElements.html: HtmlHTMLAttributeshtml>
)
})
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: UseStateTypeuseState } from 'hono/jsx'
export default function function Counter(): JSX.ElementCounter() {
const [const count: numbercount, const setCount: UpdateStateFunction<number>setCount] = useState<number>(initialState: number | (() => number)): [number, UpdateStateFunction<number>] (+1 overload)useState(0)
return (
<JSX.IntrinsicElements.div: JSX.HTMLAttributesdiv>
<JSX.IntrinsicElements.p: JSX.HTMLAttributesp>Count: {const count: numbercount}</JSX.IntrinsicElements.p: JSX.HTMLAttributesp>
<JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton EventAttributes.onClick?: ((event: MouseEvent) => void) | undefinedonClick={() => const setCount: (newState: number | ((currentState: number) => number)) => voidsetCount(const count: numbercount + 1)}>Increment</JSX.IntrinsicElements.button: ButtonHTMLAttributesbutton>
</JSX.IntrinsicElements.div: JSX.HTMLAttributesdiv>
)
}
// 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.ElementCounter 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.HTMLAttributesdiv>
<JSX.IntrinsicElements.h1: JSX.HTMLAttributesh1>Hello</JSX.IntrinsicElements.h1: JSX.HTMLAttributesh1>
<function Counter(): JSX.ElementCounter />
</JSX.IntrinsicElements.div: JSX.HTMLAttributesdiv>
)
})
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.ElementComponent() {
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: numberinit={function parseInt(string: string, radix?: number): numberConverts 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)} />
}