Renderiza JSX a HTML desarrollando un jsx-runtime con Deno
He probado a crear mi propio jsx-runtime para desarrollar un servidor que renderiza código HTML utilizando ficheros JSX en lugar de plantillas. En este post te cuento qué es JSX, cómo funciona React y un ejemplo con Deno y JSX.
Esta semana estuve probando deno, ya que aunque tenga cosas que mejorar y ahora mismo no sea un reemplazo de node, tiene características buenas como transpilar typescript de forma dinámica o tener soporte para ficheros JSX (y TSX).
En mi caso he estado acostumbrado a trabajar con ficheros JSX, en lugar de utilizar sistemas de plantillas. Y la verdad es que poder combinar Javascript y HTML me encanta, me parece ideal. Estuve probando para mi blog a no utilizar React, y las opciones que había con sistemas de plantillas funcionan pero no me terminan de convencer. Supongo que es acostumbrarse como a todo y en mi caso, me gusta utilizar JSX ya que combinan javascript con html, y me parece muy cómodo.
En este post quiero profundizar en cómo funciona los "runtimes" que procesan y ejecutan ficheros JSX de forma que pueda desarrollar el mío propio, siendo mi objetivo el renderizar un fichero JSX a HTML directamente. Esto es lo mismo que frameworks como Next.js o Remix.js basados en React, o incluso Fresh basado en Deno y Preact, que lo utilizan para enviar ficheros estáticos desde el servidor al cliente, lo que se conoce como server-side-rendering.
Vamos a ver qué son los ficheros JSX y cómo podemos hacer nuestro propio runtime.
Ficheros JSX
Los ficheros .tsx son lo mismo que los .jsx, pero tipados usando TypeScript.
Lo primero que debemos comprender es qué son los ficheros JSX, ampliamente utilizados al programar con React o sus derivados. JSX es un acrónimo de JavaScript XML, una extensión que nos permite escribir código que nos permite combinar HTML dentro de JavaScript.
Esta combinación facilita la creación de componentes que utilizaremos en nuestra interfaz de usuario (UI), y que permiten tener la lógica de programación junto con la estructura del componente.
Para poder ejecutar un fichero JSX, primero hay que transformarlo en funciones JavaScript utilizando un compilador, como son esbuild o swc, antes de ser ejecutado en el navegador.
Vamos a ver un ejemplo. Voy a crear un componente App
que contiene un simple código HTML para saludar al usuario.
const App = () => {
return (
<>
<h1>Hello, World!</h1>
<p>This is a simple JSX example compiled with esbuild or swc.</p>
</>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
Como hemos visto, esto no es compatible con Node o con nuestro navegador, y primero necesitamos transpilarlo nosotros. Para ello podemos utilizar esbuild
con el siguiente comando:
npx esbuild --bundle --loader:.jsx=jsx app.jsx
Lo que genera el siguiente código javascript:
(() => {
// app.jsx
var App = () => {
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("h1", null, "Hello, World!"), /* @__PURE__ */ React.createElement("p", null, "This is a simple JSX example compiled with esbuild or swc."));
};
ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
})();
En este código puedes ver cómo transforma el fichero jsx en código javascript preparado para ser ejecutando junto con la librería React. Este comportamiento lo puedes modificar para que en lugar de utilizar React.createElement
utilice otras funciones mediante el flag --jsx-factory=h
para poder utilizar Preact o nuestro propio runtime, que es lo que veremos a continuación.
Cómo funciona JSX con React
El proceso de transpilación de JSX por defecto añade automáticamente los imports de React, que contienen la implementación de su API y es lo que se conoce como "jsx-runtime", que contiene la lógica que procesa las funciones javascript generadas por nuestro compilador a partir de un fichero JSX. Esta es la forma en la que funciona React.
Si en lugar de instalar React desarrollamos una implementación de React diferente pero que comparta tanto su API como su JSX-Runtime, tendríamos una alternativa como puede ser Preact u otras librerías alternativas. También existen otras herramientas como Hono, que te permite crear server y client components utilizando ficheros JSX, y que son las que me gustaría probar para crear el servidor de mi blog y que veremos en el siguiente post.
Ahora, vamos a desarrollar un "jsx-runtime" simple utilizando Deno para entender cómo funciona.
Deno, probando la teoría
Una funcionalidad muy interesante de Deno es que por defecto es capaz de transpilar tanto typescript como ficheros JSX (o TSX). Posiblemente al estar hecho en rust utiliza swc y este es el que permite realizar esto. Esta funcionalidad nos permite saltarnos un paso de configuración de nuestro pipeline de node: configurar las herramientas para transpilar typescript y jsx a javascript.
Como me siento más cómodo utilizando JSX en lugar de plantillas (aunque realmente son lo mismo), quiero entender cómo funciona un jsx-runtime, y te lo voy a enseñar con un pequeño ejemplo que voy a desarrollar para renderizar componentes JSX en el lado del servidor.
Para ejecutar Typescript con Deno no tenemos que configurar prácticamente nada, y para compilar JSX únicamente tenemos que especificar las siguientes opciones en el fichero deno.json
que le indican al compilador cómo debe transpilar el jsx a javascript.
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxFactory": "jsx",
"jsxImportSource": "asdf"
},
"imports": {
"asdf/jsx-runtime": "./jsx-runtime.ts"
}
}
Esta configuración no la he encontrado explicada en ningún sitio así que brevemente lo que configuras son:
- jsx: hay varias formas que el compilador puede utilizar para convertir jsx en js. En este caso he explorado únicamente
react-jsx
. Hay otras que no son compatibles con react pero que te permiten optimizar la ejecución de tu código pre-renderizando algunos strings. - jsxFactory: indica las funciones que se van a llamar para generar los componentes. En este caso voy a utilizar las funciones
jsx / jsxs
del estándar de react, y que son las que deberemos implementar entre otras. - jsxImportSource: indica el paquete que debe importar para ejecutar el runtime. En mi caso quiero que utilice mi propio módulo local para hacer tests, por lo que lo sobreescribo en la sección imports. De cara a producción podría publicar un paquete en npm o en jsr.
Ahora viene lo divertido que básicamente es implementar las funciones jsx
y jsxs
que conforman la definición de la transformación que hemos establecido. La función jsx añade a tu árbol (DOM) un componente con un hijo y la función jsxs añade un componente con varios hijos. Hay otras funciones que se pueden implementar pero que no voy a entrar para no alargar más este post. Además, necesitamos una función que tome un árbol de componentes y genere el HTML final, que será nuestra función render
.
export function jsx(type, props, key): JSX.Element {
return { type, props: props || {}, key };
}
export function jsxs(type, props, key): JSX.Element {
return { type, props: props || {}, key };
}
export function render(element: JSX.Element): string {
const { type, props, key } = element;
if (typeof type === "function") {
return render(type(props));
}
if (!props.children) {
return "<hola></hola>";
}
if (Array.isArray(props.children)) {
const childrenString = props.children.map((child) => {
if (typeof child === "object") {
return render(child);
}
return child;
}).join("");
return `<${type}>${childrenString}</${type}>`;
}
return `<${type}>${props.children}</${type}>`;
}
Finalmente podemos escribir un fichero main.ts
que una las distintas partes:
import { App } from "./app.tsx";
import { render } from "./jsx-runtime.ts";
function greet(name: string): JSX.Element {
return App({ name });
}
console.log(render(greet("Alberto")));
Con el comando deno task dev
podemos ejecutar el fichero main.ts
que renderiza nuestro componente App.tsx
y nos muestra nuestro HTML renderizado. ¡Vamos! 👏😎
Si te ha resultado útil este artículo agradecería si te suscribes a mi newsletter. Recibirás contenido exclusivo de calidad y también me ayudarás enormemente. Cada suscripción apoya el trabajo que realizo y me permite conocer mejor los temas que te interesan, de forma que puedo mejorar los conocimientos que comparto contigo.