Zum Inhalt springen

JSX ​

Mit hono/jsx kannst du HTML in JSX-Syntax schreiben.

Obwohl hono/jsx auf dem Client funktioniert, wirst du es wahrscheinlich am häufigsten zum serverseitigen Rendern von Inhalten verwenden. Hier sind einige JSX-Funktionen, die Server und Client gemeinsam haben.

Einstellungen ​

Um JSX zu verwenden, ändere tsconfig.json:

tsconfig.json:

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx"
  }
}

Alternativ kannst du Pragma-Direktiven verwenden:

ts
/** @jsx jsx */
/** @jsxImportSource hono/jsx */

Für Deno musst du statt tsconfig.json die Datei deno.json ändern:

json
{
  "compilerOptions": {
    "jsx": "precompile",
    "jsxImportSource": "@hono/hono/jsx"
  }
}

Verwendung ​

Info

Wenn du direkt vom Schnellstart kommst, hat die Hauptdatei die Erweiterung .ts. Du musst sie in .tsx ändern, sonst kannst du die Anwendung überhaupt nicht ausführen. Passe außerdem package.json oder bei Deno deno.json entsprechend an. Beispielsweise muss das Entwicklungsskript statt bun run --hot src/index.ts den Befehl bun run --hot src/index.tsx enthalten.

index.tsx:

tsx
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'

const app = new Hono()

const Layout: FC = (props) => {
  return (
    <html>
      <body>{props.children}</body>
    </html>
  )
}

const Top: FC<{ messages: string[] }> = (props: {
  messages: string[]
}) => {
  return (
    <Layout>
      <h1>Hello Hono!</h1>
      <ul>
        {props.messages.map((message) => {
          return <li>{message}!!</li>
        })}
      </ul>
    </Layout>
  )
}

app.get('/', (c) => {
  const messages = ['Good Morning', 'Good Evening', 'Good Night']
  return c.html(<Top messages={messages} />)
})

export default app

Metadaten in den Dokumentkopf verschieben ​

Du kannst Metadaten-Tags wie <title>, <link> und <meta> direkt in deinen Komponenten schreiben. Diese Tags werden automatisch in den Abschnitt <head> des Dokuments verschoben. Das ist besonders nützlich, wenn das Element <head> weit von der Komponente entfernt gerendert wird, die die passenden Metadaten bestimmt.

tsx
import { Hono } from 'hono'

const app = new Hono()

app.use('*', async (c, next) => {
  c.setRenderer((content) => {
    return c.html(
      <html>
        <head></head>
        <body>{content}</body>
      </html>
    )
  })
  await next()
})

app.get('/about', (c) => {
  return c.render(
    <>
      <title>About Page</title>
      <meta name='description' content='This is the about page.' />
      about page content
    </>
  )
})

export default app

Info

Dabei werden vorhandene Elemente nicht entfernt. Später auftretende Elemente werden am Ende hinzugefügt. Wenn beispielsweise in deinem <head> bereits <title>Default</title> steht und eine Komponente <title>Page Title</title> rendert, erscheinen beide Titel im Dokumentkopf.

Fragment ​

Mit Fragment kannst du mehrere Elemente gruppieren, ohne zusätzliche Knoten hinzuzufügen:

tsx
import { Fragment } from 'hono/jsx'

const List = () => (
  <Fragment>
    <p>first child</p>
    <p>second child</p>
    <p>third child</p>
  </Fragment>
)

Bei entsprechender Konfiguration kannst du auch <></> schreiben.

tsx
const List = () => (
  <>
    <p>first child</p>
    <p>second child</p>
    <p>third child</p>
  </>
)

PropsWithChildren ​

Mit PropsWithChildren kannst du ein untergeordnetes Element in einer Funktionskomponente korrekt typisieren lassen.

tsx
import { PropsWithChildren } from 'hono/jsx'

type Post = {
  id: number
  title: string
}

function Component({ title, children }: PropsWithChildren<Post>) {
  return (
    <div>
      <h1>{title}</h1>
      {children}
    </div>
  )
}

Unverarbeitetes HTML einfügen ​

Um HTML direkt einzufügen, verwende dangerouslySetInnerHTML:

tsx
app.get('/foo', (c) => {
  const inner = { __html: 'JSX &middot; SSR' }
  const Div = <div dangerouslySetInnerHTML={inner} />
})

Memoisierung ​

Optimiere deine Komponenten, indem du berechnete Zeichenketten mit memo memoisiert speicherst:

tsx
import { memo } from 'hono/jsx'

const Header = memo(() => <header>Welcome to Hono</header>)
const Footer = memo(() => <footer>Powered by Hono</footer>)
const Layout = (
  <div>
    <Header />
    <p>Hono is cool!</p>
    <Footer />
  </div>
)

Kontext ​

Mit useContext kannst du Daten global über jede Ebene des Komponentenbaums hinweg teilen, ohne Werte über Props weiterzugeben.

tsx
import type { FC } from 'hono/jsx'
import { createContext, useContext } from 'hono/jsx'

const themes = {
  light: {
    color: '#000000',
    background: '#eeeeee',
  },
  dark: {
    color: '#ffffff',
    background: '#222222',
  },
}

const ThemeContext = createContext(themes.light)

const Button: FC = () => {
  const theme = useContext(ThemeContext)
  return <button style={theme}>Push!</button>
}

const Toolbar: FC = () => {
  return (
    <div>
      <Button />
    </div>
  )
}

// ...

app.get('/', (c) => {
  return c.html(
    <div>
      <ThemeContext.Provider value={themes.dark}>
        <Toolbar />
      </ThemeContext.Provider>
    </div>
  )
})

Asynchrone Komponenten ​

hono/jsx unterstützt asynchrone Komponenten, sodass du async/await in deiner Komponente verwenden kannst. Wenn du sie mit c.html() renderst, wird automatisch auf das Ergebnis gewartet.

tsx
const AsyncComponent = async () => {
  await new Promise((r) => setTimeout(r, 1000)) // sleep 1s
  return <div>Done!</div>
}

app.get('/', (c) => {
  return c.html(
    <html>
      <body>
        <AsyncComponent />
      </body>
    </html>
  )
})

Suspense Experimental ​

Die React-ähnliche Funktion Suspense ist verfügbar. Wenn du eine asynchrone Komponente mit Suspense umschließt, wird zunächst der Fallback-Inhalt gerendert. Sobald das Promise aufgelöst ist, wird der erwartete Inhalt angezeigt. Du kannst sie mit renderToReadableStream() verwenden.

tsx
import { renderToReadableStream, Suspense } from 'hono/jsx/streaming'

//...

app.get('/', (c) => {
  const stream = renderToReadableStream(
    <html>
      <body>
        <Suspense fallback={<div>loading...</div>}>
          <Component />
        </Suspense>
      </body>
    </html>
  )
  return c.body(stream, {
    headers: {
      'Content-Type': 'text/html; charset=UTF-8',
      'Transfer-Encoding': 'chunked',
    },
  })
})

ErrorBoundary Experimental ​

Mit ErrorBoundary kannst du Fehler in untergeordneten Komponenten abfangen.

Im folgenden Beispiel wird bei einem Fehler der in fallback angegebene Inhalt angezeigt.

tsx
function SyncComponent() {
  throw new Error('Error')
  return <div>Hello</div>
}

app.get('/sync', async (c) => {
  return c.html(
    <html>
      <body>
        <ErrorBoundary fallback={<div>Out of Service</div>}>
          <SyncComponent />
        </ErrorBoundary>
      </body>
    </html>
  )
})

ErrorBoundary kann auch mit asynchronen Komponenten und Suspense verwendet werden.

tsx
async function AsyncComponent() {
  await new Promise((resolve) => setTimeout(resolve, 2000))
  throw new Error('Error')
  return <div>Hello</div>
}

app.get('/with-suspense', async (c) => {
  return c.html(
    <html>
      <body>
        <ErrorBoundary fallback={<div>Out of Service</div>}>
          <Suspense fallback={<div>Loading...</div>}>
            <AsyncComponent />
          </Suspense>
        </ErrorBoundary>
      </body>
    </html>
  )
})

StreamingContext Experimental ​

Mit StreamingContext kannst du Streaming-Komponenten wie Suspense und ErrorBoundary konfigurieren. Das ist nützlich, um den von diesen Komponenten generierten script-Tags Nonce-Werte für die Content Security Policy (CSP) hinzuzufügen.

tsx
import { Suspense, StreamingContext } from 'hono/jsx/streaming'

// ...

app.get('/', (c) => {
  const stream = renderToReadableStream(
    <html>
      <body>
        <StreamingContext
          value={{ scriptNonce: 'random-nonce-value' }}
        >
          <Suspense fallback={<div>Loading...</div>}>
            <AsyncComponent />
          </Suspense>
        </StreamingContext>
      </body>
    </html>
  )

  return c.body(stream, {
    headers: {
      'Content-Type': 'text/html; charset=UTF-8',
      'Transfer-Encoding': 'chunked',
      'Content-Security-Policy':
        "script-src 'nonce-random-nonce-value'",
    },
  })
})

Der Wert von scriptNonce wird automatisch zu allen <script>-Tags hinzugefügt, die die Komponenten Suspense und ErrorBoundary generieren.

Integration mit html-Middleware ​

Kombiniere die JSX- und HTML-Middleware für leistungsfähige Vorlagen. Ausführliche Informationen findest du in der Dokumentation zur HTML-Middleware.

tsx
import { Hono } from 'hono'
import { html } from 'hono/html'

const app = new Hono()

interface SiteData {
  title: string
  children?: any
}

const Layout = (props: SiteData) =>
  html`<!doctype html>
    <html>
      <head>
        <title>${props.title}</title>
      </head>
      <body>
        ${props.children}
      </body>
    </html>`

const Content = (props: { siteData: SiteData; name: string }) => (
  <Layout {...props.siteData}>
    <h1>Hello {props.name}</h1>
  </Layout>
)

app.get('/:name', (c) => {
  const { name } = c.req.param()
  const props = {
    name: name,
    siteData: {
      title: 'JSX with html sample',
    },
  }
  return c.html(<Content {...props} />)
})

export default app

Mit JSX-Renderer-Middleware ​

Die JSX-Renderer-Middleware erleichtert dir das Erstellen von HTML-Seiten mit JSX.

Typdefinitionen überschreiben ​

Du kannst die Typdefinition überschreiben, um eigene Elemente und Attribute hinzuzufügen.

ts
declare module 'hono/jsx' {
  namespace JSX {
    interface IntrinsicElements {
      'my-custom-element': HTMLAttributes & {
        'x-event'?: 'click' | 'scroll'
      }
    }
  }
}

Veröffentlicht unter der MIT-Lizenz.