wandres.dev
INTEGRACIONES DE FRAMEWORK · React, Solid, Vue, Svelte

astro add react: integraciones y renderers

Qué ocurre realmente cuando ejecutas astro add react: la integración que se instala, el renderer que registra en tu configuración y sus dos mitades —el entrypoint de servidor que imprime HTML y el de cliente que hidrata la isla—. Cómo Astro decide qué renderer atiende cada fichero, cómo conviven varios frameworks de UI en un mismo proyecto y cómo se resuelve el conflicto cuando React, Preact y Solid reclaman el mismo .jsx con include y exclude.

⏱ 16 min

Astro no habla React, Vue, Svelte ni Solid de nacimiento: habla HTML. Lo que le permite renderizar cualquiera de esos frameworks es una pieza enchufable llamada renderer, y el comando astro add react no obra ninguna magia: instala esa pieza, registra sus dependencias y engancha sus dos mitades —una que corre en el servidor y otra que despierta en el cliente—. Entender ese mecanismo es dejar de ver las integraciones como cajas negras y empezar a verlas como lo que son: adaptadores que traducen el modelo de un framework al modelo de islas de Astro.

🎯 Al terminar esta lección sabrás
  • Ejecutar astro add react y saber exactamente qué instala, edita y registra por ti.
  • Abrir la anatomía de un renderer: sus entrypoints de servidor y de cliente, y la función check.
  • Convivir con varios frameworks de UI en un mismo proyecto sin fricción.
  • Resolver el conflicto cuando dos renderers reclaman el mismo .jsx con include y exclude.

Qué hace astro add react

Añadir un framework a mano son tres pasos frágiles: instalar el paquete de la integración con sus dependencias peer, importarlo en astro.config.mjs y registrarlo en el array integrations sin romper la sintaxis. astro add react ejecuta los tres por ti y te enseña el diff antes de tocar un solo fichero.

npx astro add react
# instala @astrojs/react y sus dependencias peer: react, react-dom y sus tipos
# edita astro.config.mjs anadiendo el import y react() al array integrations
# todo previa confirmacion, mostrando los cambios antes de escribirlos

El resultado es una configuración mínima y declarativa. La integración es una función que, al invocarse, devuelve un objeto de hooks; colocarla en el array no la ejecuta todavía, solo la enrola para que Astro la llame en los momentos oportunos de su ciclo de vida.

// astro.config.mjs
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';

export default defineConfig({
  integrations: [react()],
});

Conviene fijar el vocabulario. Una integración es el paquete que enrolas en integrations; un renderer es lo que esa integración registra por dentro para enseñarle a Astro a renderizar un framework concreto. La integración de React hace dos cosas en el arranque: añade un plugin de Vite que compila JSX y llama a addRenderer con el adaptador que traduce componentes de React a HTML y de vuelta a interactividad. Ese renderer es el verdadero protagonista de esta lección.

Una integración por dentro: el hook que registra el renderer

Una integración de Astro es un objeto con un name y un mapa de hooks, cada uno correspondiente a una fase del ciclo de vida del framework. El hook decisivo aquí es astro:config:setup, que recibe utilidades para modificar la configuración, y entre ellas addRenderer. Así, muy simplificada, se ve la forma de lo que @astrojs/react registra.

// forma simplificada de lo que hace @astrojs/react
export default function react() {
  return {
    name: '@astrojs/react',
    hooks: {
      'astro:config:setup': ({ addRenderer, updateConfig }) => {
        addRenderer({
          name: '@astrojs/react',
          clientEntrypoint: '@astrojs/react/client.js',
          serverEntrypoint: '@astrojs/react/server.js',
        });
        updateConfig({ vite: { /* plugin que compila JSX */ } });
      },
    },
  };
}

Un renderer se reduce a tres datos: un nombre y dos rutas de entrada. El serverEntrypoint es un módulo que exporta una función check, que decide si un componente le pertenece, y una función de renderizado que lo convierte en una cadena de HTML. El clientEntrypoint es el módulo que, en el navegador, toma ese HTML ya impreso y lo hidrata. Registrar un framework es, literalmente, entregarle a Astro esas dos direcciones.

🧩

Integración

El paquete que enrolas en integrations. Engancha en los hooks de Astro y, en su arranque, registra uno o varios renderers.

🖨️

serverEntrypoint

Corre en el build o en la petición. Exporta check para reclamar sus componentes y produce el HTML inicial de la isla.

💧

clientEntrypoint

Solo se carga si hay una directiva client:. Toma el HTML impreso y lo hidrata en el navegador con estado y eventos.

Dos mitades: el servidor imprime, el cliente despierta

Esta separación en dos entrypoints es la que da sentido a toda la arquitectura de islas. Cuando colocas un componente de framework sin directiva, Astro invoca su serverEntrypoint, obtiene el HTML y lo incrusta en la página: el clientEntrypoint no se carga nunca y el visitante recibe cero JavaScript de ese framework. Solo cuando añades una directiva client: Astro incluye el clientEntrypoint en el bundle y programa la hidratación de esa isla concreta.

---
import Panel from '../components/Panel.astro';   // .astro, solo servidor
import Filtro from '../components/Filtro.jsx';    // renderer de React
---
<Panel titulo="Catalogo" />
<Filtro client:visible />

Aquí Filtro pasa por el serverEntrypoint de React para producir su marcado inicial, igual que Panel; la diferencia es que client:visible engancha además el clientEntrypoint, que se descargará y ejecutará cuando la isla entre en pantalla. El renderer, en resumen, es un traductor bidireccional: del componente al HTML en el servidor, y del HTML al componente vivo en el cliente.

flowchart TD
ADD[astro add react] --> INT[integracion en el array integrations]
INT --> SETUP[hook astro config setup]
SETUP --> REND[addRenderer]
REND --> S[serverEntrypoint]
REND --> C[clientEntrypoint]
S --> HTML[HTML inicial en el build]
C --> HID[hidratacion solo si hay directiva client]
style REND fill:#89b4fa,color:#11111b
style HTML fill:#a6e3a1,color:#11111b
style HID fill:#f9e2af,color:#11111b
ℹ️
Sin directiva no se carga el cliente

El clientEntrypoint de un renderer es código diferido por diseño: Astro solo lo incluye en el bundle de una página si esa página tiene al menos una isla de ese framework con directiva client:. Una página llena de componentes de React usados sin directiva no descarga ni un byte de React. Es la misma economía de las islas aplicada al propio renderer: pagas su mitad de cliente únicamente donde pediste interactividad.

Varios frameworks en un mismo proyecto

Como cada integración registra su propio renderer, nada impide enrolar varias a la vez. Un proyecto puede servir islas de React, de Vue y de Svelte en la misma página, cada una con su framework cargado solo dentro de su isla.

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import svelte from '@astrojs/svelte';

export default defineConfig({
  integrations: [react(), vue(), svelte()],
});

Ante un componente, Astro decide qué renderer lo atiende por dos vías. La primera es la extensión: un .vue solo lo entiende el renderer de Vue, un .svelte solo el de Svelte, y ahí no hay ambigüedad posible. La segunda es la función check de cada renderer, que se consulta para las extensiones que varios comparten. El problema aparece precisamente con esas: .jsx y .tsx los reclaman por igual React, Preact y Solid, porque los tres hablan JSX y ninguno puede distinguir a simple vista un componente ajeno.

Cuando dos renderers reclaman el mismo .jsx

Si tienes React y Preact —o React y Solid— en el mismo proyecto, Astro se topa con un .jsx que dos renderers dicen poder renderizar y aborta el build con un error de ambigüedad, porque adivinar sería peor que fallar. La solución es acotar el territorio de cada uno con las opciones include y exclude, que aceptan globs de rutas.

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import preact from '@astrojs/preact';

export default defineConfig({
  integrations: [
    react({ include: ['**/react/**'] }),
    preact({ include: ['**/preact/**'] }),
  ],
});

Con esta configuración, un .jsx bajo una carpeta react/ lo compila React y uno bajo preact/ lo compila Preact; la ambigüedad desaparece porque cada renderer solo reclama los ficheros de su glob. Vue y Svelte nunca necesitan esto: su extensión es única y los identifica sin lugar a duda. La regla mental es nítida —el conflicto vive únicamente entre frameworks que comparten sintaxis de fichero, y se resuelve dibujando fronteras de carpeta explícitas.

Una integración es la frontera entre un modelo mental y otro

Merece la pena detenerse en lo que de verdad ocurre cuando enrolas un framework en Astro, porque revela una idea de arquitectura que trasciende a este framework concreto. React, Vue, Solid y Svelte no son librerías intercambiables con la misma forma: cada uno tiene su propio modelo de cómo un componente se convierte en HTML y de cómo ese HTML vuelve a ser interactivo. React reconcilia un árbol virtual; Solid teje reactividad de grano fino sin árbol virtual; Svelte compila el componente a JavaScript imperativo; Vue orquesta proxies reactivos. Cuatro cosmovisiones distintas de un mismo problema. Lo que hace un renderer es reducir todas esas cosmovisiones a un contrato común y minúsculo: una función que produce una cadena de HTML en el servidor y una función que la despierta en el cliente. Ese contrato es la interfaz que le permite a Astro ser agnóstico —no sabe nada de árboles virtuales ni de señales, solo sabe pedir HTML y pedir hidratación— y es también lo que te permite a ti mezclar frameworks en una página sin que colisionen, porque ninguno reclama ser el runtime de todo el sitio: cada uno vive encapsulado tras el mismo contrato de dos funciones. Aquí hay una lección de diseño que vale para cualquier sistema extensible. El poder de una arquitectura de plugins no está en la lista de cosas que soporta hoy, sino en la estrechez del contrato que exige para soportar una nueva. Astro no tuvo que anticipar cada framework de UI que existiría; definió una interfaz de renderer tan pequeña que cualquier framework, presente o futuro, puede satisfacerla con un adaptador de pocas líneas. Cuando el contrato es estrecho y las implementaciones son gruesas, el sistema escala sin que su núcleo crezca. Por eso astro add react no es un caso especial cableado en Astro: es una integración más, indistinguible en forma de las que puedas escribir tú, hablando el mismo idioma de dos entrypoints. Aprender a ver esa frontera —dónde termina el núcleo agnóstico y dónde empieza el adaptador específico— es aprender a diseñar sistemas que envejecen bien.

⚔️ Registra frameworks y observa sus mitades
  1. Ejecuta npx astro add react en un proyecto limpio, revisa el diff propuesto y localiza en astro.config.mjs el import y la entrada react() que ha añadido.
  2. Coloca un componente de React sin directiva en una página, construye e inspecciona la red: confirma que no se descarga JavaScript de React porque el clientEntrypoint no se cargó.
  3. Añade client:load a ese mismo componente, reconstruye y verifica que ahora sí aparece el bundle de React para esa isla.
  4. Enrola también Preact junto a React y crea un .jsx sin acotar; observa el error de ambigüedad y resuélvelo con include para cada integración.