Aller au contenu

JSX ​

Vous pouvez écrire du HTML avec la syntaxe JSX grâce à hono/jsx.

Bien que hono/jsx fonctionne côté client, vous l'utiliserez probablement surtout pour rendre du contenu côté serveur. Voici quelques fonctionnalités JSX communes au serveur et au client.

Configuration ​

Pour utiliser JSX, modifiez tsconfig.json :

tsconfig.json :

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

Vous pouvez aussi utiliser les directives pragma :

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

Avec Deno, vous devez modifier deno.json plutôt que tsconfig.json :

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

Utilisation ​

Information

Si vous venez directement du démarrage rapide, le fichier principal porte l'extension .ts. Vous devez la remplacer par .tsx, sinon l'application ne pourra pas fonctionner. Modifiez aussi package.json (ou deno.json avec Deno) pour tenir compte de ce changement (par exemple, remplacez bun run --hot src/index.ts par bun run --hot src/index.tsx dans le script de développement).

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

Déplacer les métadonnées dans l'en-tête ​

Vous pouvez écrire les balises de métadonnées du document, telles que <title>, <link> et <meta>, directement dans vos composants. Elles seront automatiquement déplacées dans la section <head> du document. C'est particulièrement utile lorsque l'élément <head> est rendu loin du composant qui détermine les métadonnées appropriées.

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

Information

Lors de ce déplacement, les éléments existants ne sont pas supprimés. Les éléments rencontrés plus tard sont ajoutés à la fin. Par exemple, si votre <head> contient <title>Default</title> et qu'un composant rend <title>Page Title</title>, les deux titres apparaîtront dans l'en-tête.

Fragment ​

Utilisez Fragment pour regrouper plusieurs éléments sans ajouter de nœuds supplémentaires :

tsx
import { Fragment } from 'hono/jsx'

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

Vous pouvez aussi utiliser <></> si la configuration le permet.

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

PropsWithChildren ​

Vous pouvez utiliser PropsWithChildren pour inférer correctement le type d'un élément enfant dans un composant fonctionnel.

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>
  )
}

Insérer du HTML brut ​

Pour insérer directement du HTML, utilisez dangerouslySetInnerHTML :

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

Mémoïsation ​

Optimisez vos composants en mémorisant les chaînes calculées avec memo :

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>
)

Contexte ​

Avec useContext, vous pouvez partager des données globalement à n'importe quel niveau de l'arbre de composants, sans transmettre les valeurs via les props.

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>
  )
})

Composant asynchrone ​

hono/jsx prend en charge les composants asynchrones ; vous pouvez donc utiliser async/await dans votre composant. Si vous le rendez avec c.html(), l'attente se fait automatiquement.

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 Expérimental ​

La fonctionnalité Suspense, semblable à celle de React, est disponible. Si vous enveloppez un composant asynchrone dans Suspense, le contenu de remplacement est rendu d'abord. Une fois la Promise résolue, le contenu attendu est affiché. Vous pouvez l'utiliser avec renderToReadableStream().

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 Expérimental ​

Vous pouvez intercepter les erreurs des composants enfants avec ErrorBoundary.

Dans l'exemple ci-dessous, le contenu indiqué dans fallback sera affiché si une erreur survient.

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 peut aussi être utilisé avec des composants asynchrones et Suspense.

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 Expérimental ​

Vous pouvez utiliser StreamingContext pour configurer des composants de streaming tels que Suspense et ErrorBoundary. C'est utile pour ajouter des valeurs nonce aux balises script générées par ces composants dans le cadre de la politique de sécurité du contenu (CSP).

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'",
    },
  })
})

La valeur scriptNonce est automatiquement ajoutée aux balises <script> générées par les composants Suspense et ErrorBoundary.

Intégration avec le middleware HTML ​

Combinez les middlewares JSX et HTML pour créer des modèles puissants. Pour en savoir plus, consultez la documentation du middleware HTML.

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

Avec le middleware de rendu JSX ​

Le middleware de rendu JSX permet de créer plus facilement des pages HTML avec JSX.

Redéfinir les types ​

Vous pouvez redéfinir les types pour ajouter vos propres éléments et attributs.

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

Publié sous licence MIT.