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.
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 escribirlosEl 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.mjsimport { 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/reactexport 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.
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 servidorimport 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.
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.