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 :
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}Vous pouvez aussi utiliser les directives pragma :
/** @jsx jsx */
/** @jsxImportSource hono/jsx */Avec Deno, vous devez modifier deno.json plutôt que tsconfig.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 :
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 appDé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.
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 appInformation
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 :
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.
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.
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 :
app.get('/foo', (c) => {
const inner = { __html: 'JSX · SSR' }
const Div = <div dangerouslySetInnerHTML={inner} />
})Mémoïsation
Optimisez vos composants en mémorisant les chaînes calculées avec memo :
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.
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.
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().
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.
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.
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).
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.
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 appAvec 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.
declare module 'hono/jsx' {
namespace JSX {
interface IntrinsicElements {
'my-custom-element': HTMLAttributes & {
'x-event'?: 'click' | 'scroll'
}
}
}
}