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

Anidar frameworks distintos y pasar slots

Dentro de su propio JSX o plantilla, un componente de framework solo puede renderizar componentes del mismo framework; para mezclar React con Svelte o Vue con Solid, el terreno neutral es la plantilla .astro, que renderiza una isla y le pasa otra como slot. La regla dura que lo hace posible: las directivas client solo existen en ficheros .astro, nunca dentro del JSX de un framework, de modo que un componente anidado por importación comparte la hidratación de su padre, mientras que uno pasado como slot desde Astro conserva su propia directiva e hidrata por separado.

⏱ 17 min

En cuanto un proyecto mezcla varios frameworks aparece la pregunta inevitable: ¿puede un componente de React contener uno de Svelte? La respuesta corta es no, no directamente, y la razón no es un capricho de Astro, sino una consecuencia de cómo funciona cada framework. Pero hay una vía, y es la misma pieza que ha sostenido todo el nivel: la plantilla .astro como terreno neutral donde las islas se componen. Entender qué se puede anidar dónde, y sobre todo dónde vive la directiva client:, es lo que te permite construir interfaces que combinan frameworks sin que se pisen.

🎯 Al terminar esta lección sabrás
  • Entender por qué un componente de framework solo renderiza directamente a otros del mismo framework.
  • Componer islas de frameworks distintos pasándolas como slots desde una plantilla .astro.
  • Fijar la regla de que las directivas client: solo existen en ficheros .astro, nunca en JSX de framework.
  • Distinguir la hidratación compartida del anidamiento por importación de la hidratación independiente por slot.

Cada framework solo renderiza a los suyos

Dentro del JSX o la plantilla de un componente, cada framework habita su propio mundo. Un componente de React puede importar y colocar otro componente de React en su árbol; ese es el modelo de composición normal del framework y funciona sin intervención de Astro. Lo que no puede es colocar un componente de Svelte o de Vue en su JSX, porque React no sabe renderizar nada que no sea React: su reconciliador espera elementos de React, no un componente compilado por otro motor.

// Panel.jsx de React
import Fila from './Fila.jsx';         // OK: React dentro de React
// import Grafico from './G.svelte';   // no tiene sentido: React no renderiza Svelte

export default function Panel({ children }) {
  return (
    <div class="panel">
      <Fila />
      {children}
    </div>
  );
}

Esta frontera es lógica, no arbitraria. Cada framework tiene su propio formato de elemento interno y su propio ciclo de vida; mezclarlos en un mismo árbol exigiría que uno supiera hablar el idioma del otro, cosa que ninguno hace. Por eso la composición entre frameworks distintos no ocurre dentro de un componente, sino un nivel por encima, en el único lugar que los conoce a todos.

Conviene subrayar que esta no es una restricción que Astro imponga, sino una que hereda. En una aplicación pura de cualquiera de estos frameworks ocurriría exactamente lo mismo: React nunca ha sabido montar un componente de Vue en su árbol, ni Svelte hospedar uno de Solid. Lo que Astro añade no es la barrera —que ya existía—, sino la salida: un terreno por encima de los frameworks donde la barrera deja de importar.

Componer frameworks distintos desde Astro

Ese lugar es la plantilla .astro. Un fichero de Astro puede renderizar una isla de cualquier framework y pasarle, como contenido anidado, una isla de otro framework. Astro renderiza el hijo por su cuenta y lo entrega al padre como HTML, que este coloca en su {children} o en el slot correspondiente.

---
import Marco from '../components/Marco.jsx';        // React
import Grafico from '../components/Grafico.svelte';  // Svelte
---
<Marco client:load>
  <Grafico client:visible />
</Marco>

Aquí un marco de React envuelve un gráfico de Svelte, algo imposible dentro del JSX de Marco, pero trivial desde la plantilla. Astro renderiza Grafico a HTML, se lo pasa a Marco como children, y Marco lo sitúa donde su código indique. La plantilla .astro actúa de adaptador universal: al hablar el contrato común de renderers, puede encajar piezas de motores que jamás se entenderían entre sí directamente. Recuerda, eso sí, la ley de la lección anterior: para Marco, ese children es HTML ya impreso e inerte; no puede regenerarlo con su propio estado.

El mismo puente sirve para los slots con nombre. Si el componente de framework declara varios huecos, diriges contenido de Astro a cada uno con el atributo slot sobre el elemento anidado, exactamente como harías hacia un .astro. Astro renderiza cada fragmento por separado y lo entrega al hueco que le corresponde, sea ese fragmento texto, otro componente .astro o incluso otra isla.

---
import Ficha from '../components/Ficha.jsx';   // React con props titulo y pie
import Sello from '../components/Sello.astro';  // .astro estatico
---
<Ficha client:load>
  <Sello slot="titulo" />
  <p>Cuerpo en el slot por defecto.</p>
  <small slot="pie">Actualizado hoy</small>
</Ficha>

Como viste en la segunda lección, un slot="titulo" de Astro aterriza en la isla de React como una prop titulo, el slot="pie" como una prop pie, y el resto del contenido como children. El framework no percibe que ese marcado nació en una plantilla de Astro ni que uno de los fragmentos era a su vez un componente .astro: solo ve HTML llegando a sus huecos. Esa opacidad es justo lo que vuelve universal al puente, porque le da igual de qué motor —o de ninguno— procede lo que recibe.

La directiva client: solo existe en .astro

Aquí está el detalle que lo gobierna todo y que conviene grabar: las directivas client: son una característica de la plantilla de Astro, no del JSX de ningún framework. Solo puedes escribir client:load, client:visible y compañía sobre un componente colocado en un fichero .astro. Dentro del JSX de React o de la plantilla de Vue, esa sintaxis no existe y no tendría a quién dirigirse.

La consecuencia es doble y precisa. Un componente que anidas por importación dentro del JSX de otro —como Fila dentro de Panel— no puede llevar directiva propia: forma parte del árbol del padre y se hidrata con él, como una sola isla indivisible. Un componente que pasas como slot desde una plantilla .astro —como Grafico dentro de Marco— conserva su propia directiva client: y se hidrata por su cuenta, como una isla independiente que resulta estar visualmente dentro de otra.

De ahí sale una prueba mental rápida: pregúntate por dónde entró el componente. Si lo escribiste en el JSX o la plantilla de otro framework, es parte de esa isla y no negocia su hidratación. Si lo colocaste en un fichero .astro, es una isla por derecho propio, con su directiva y su momento. El fichero donde vive la etiqueta, no su posición visual en la página, decide su destino.

🧬

Anidado por importación

Un componente usado en el JSX del padre. Mismo framework obligatorio. Sin directiva propia. Se hidrata junto al padre como una única isla.

🧩

Pasado como slot

Un componente colocado desde la plantilla .astro como contenido anidado. Cualquier framework. Conserva su directiva. Se hidrata por separado.

Islas dentro de islas: quién hidrata a quién

Reunir las dos ideas resuelve casi cualquier duda sobre anidamiento. Cuando un framework contiene a otro por importación —solo posible dentro del mismo framework—, hay una sola isla: una directiva, un momento de hidratación, un bundle. Cuando la composición ocurre por slot desde Astro, hay tantas islas como componentes con directiva, cada una hidratándose en su propio momento, aunque el HTML de una quede anidado dentro del de otra.

---
import Tabs from '../components/Tabs.jsx';        // React, la isla contenedora
import Mapa from '../components/Mapa.svelte';      // Svelte, isla independiente
import Aviso from '../components/Aviso.astro';     // solo servidor, cero JS
---
<Tabs client:load>
  <Aviso slot="cabecera" />
  <Mapa client:visible slot="cuerpo" />
</Tabs>

En esta escena conviven tres regímenes a la vez: Tabs es una isla de React que se hidrata al cargar; Mapa es una isla de Svelte que se hidrata al entrar en pantalla, independiente de Tabs aunque viva en su slot cuerpo; y Aviso es un .astro estático que nunca envía JavaScript. Tres piezas, tres destinos de ejecución distintos, coordinadas por una plantilla que no pertenece a ningún framework. Ese es el patrón que te deja construir interfaces mixtas sin renunciar al control fino de cuánto JavaScript paga cada región.

Y nada impide encadenar más niveles: la isla contenedora puede recibir por slot otra isla que a su vez envuelva a una tercera, cada una con su directiva y su framework. Mientras cada salto de composición suba a una plantilla .astro, la profundidad no tiene tope y cada capa conserva su destino de hidratación propio. La regla que resume todo el asunto es breve —la importación fija la pertenencia a un árbol y funde la hidratación; el slot preserva la independencia y la separa— y con ella decides, en cada anidamiento, si dos piezas son una isla o dos.

flowchart TD
A[plantilla astro] --> T[isla react Tabs con client load]
A --> M[isla svelte Mapa con client visible]
A --> AV[componente astro Aviso sin JS]
T --> F[Fila de react anidada por import]
F -.comparte hidratacion.-> T
M -.hidratacion independiente.-> A
style A fill:#89b4fa,color:#11111b
style T fill:#f9e2af,color:#11111b
style M fill:#f9e2af,color:#11111b
style AV fill:#a6e3a1,color:#11111b
⚠️
No busques una directiva client: dentro del JSX

Si intentas escribir client:idle sobre un componente dentro del JSX de React, no obtendrás ese comportamiento: allí es solo un atributo desconocido que React ignora o rechaza. La hidratación se decide en la plantilla .astro, no en el árbol del framework. Cuando quieras que una pieza anidada se hidrate por su cuenta, no la importes dentro del JSX del padre: sácala a la plantilla y pásala como slot, que es donde la directiva tiene sentido y efecto.

📝
Los slots reenviados llegan como HTML, no como componente vivo

Cuando pasas una isla como slot a otra, el padre recibe el HTML renderizado del hijo, envuelto en un marcador que Astro coloca, no una referencia al componente. El padre puede posicionar ese bloque, pero no inspeccionarlo ni comunicarse con él por props. Si el hijo es a su vez una isla con su propia directiva, cobrará vida por su cuenta; pero padre e hijo no comparten estado por el hecho de estar anidados. Compartirlo exige el mecanismo de la próxima lección.

ℹ️
El elemento astro-slot que envuelve lo reenviado

Cuando Astro entrega contenido a una isla, lo envuelve en un elemento marcador —astro-slot— que el framework adopta durante la hidratación sin volver a renderizarlo. Verlo en el HTML explica de un vistazo por qué el contenido reenviado es inerte para el padre: no forma parte de su árbol de componentes, es un injerto que ocupa un hueco. Ese marcador es la costura visible entre lo que renderizó Astro y lo que renderizó el framework, y encontrarlo en el inspector suele despejar la confusión de por qué un hijo no reacciona.

La composición entre frameworks es un problema de fronteras, no de anidamiento

El anidamiento de frameworks distintos parece a primera vista una cuestión de jerarquía —quién va dentro de quién— pero en realidad es una cuestión de fronteras, y verlo así aclara toda la mecánica de golpe. Cada framework define una región de espacio donde su modelo de componentes es la ley: dentro del JSX de React, todo es React y nada más puede existir; dentro de la plantilla de Svelte, todo es Svelte. Esas regiones son cerradas por diseño, porque un framework no sabe hospedar el ciclo de vida de otro. La plantilla .astro, en cambio, no es la región de ningún framework: es tierra de nadie, o mejor, tierra de todos, el único lugar cuyo idioma es el contrato común de renderers y no el dialecto interno de un motor concreto. Por eso la composición entre frameworks solo puede ocurrir ahí, en la frontera, nunca en el interior de una región. Y por eso las directivas client: viven exactamente ahí y en ningún otro sitio: la hidratación es una decisión sobre la frontera —cuándo esta región cerrada de interactividad cobra vida—, y solo tiene sentido enunciarla desde el lugar neutral que ve las fronteras, no desde dentro de una de ellas, que no puede hablar de sí misma. Esta distinción entre lo que ocurre dentro de una región y lo que ocurre en sus bordes es uno de los patrones más profundos de la ingeniería de sistemas. Un módulo, un servicio, un proceso, un componente: todos son regiones con un interior gobernado por sus propias reglas y un borde donde negocian con el exterior mediante un contrato explícito. Confundir los dos niveles —querer meter la lógica de frontera dentro de la región, o esperar que dos interiores se comuniquen sin pasar por sus bordes— es la fuente de una clase entera de errores de arquitectura. Astro te enseña la disciplina correcta casi sin que lo notes: las islas se componen en la frontera, se hidratan desde la frontera, y su interior permanece encapsulado y ajeno a los demás. Cuando interiorizas que anidar no es meter una cosa dentro de otra sino conectar dos interiores a través de un borde común, dejas de pelear contra el modelo y empiezas a diseñar con él.

⚔️ Compón una interfaz de frameworks mezclados
  1. Crea un Marco.jsx de React que muestre su {children} y pásale desde una plantilla .astro un componente de Svelte como contenido anidado; confirma que se renderiza.
  2. Da a cada uno su propia directiva —client:load al marco, client:visible al hijo— e inspecciona la red para verificar que se hidratan en momentos distintos.
  3. Intenta importar el componente de Svelte dentro del JSX del marco y comprueba que no funciona; razona por qué la composición debe subir a la plantilla.
  4. Anida un componente del mismo framework por importación dentro del marco y confirma que no admite directiva propia y se hidrata junto al padre como una sola isla.