Nexanexa
Paso 9

Islas interactivas

Para lo que sí necesita un framework real — un dashboard, un carrito complejo — sin que el resto del sitio deje de ser HTML-first.

¿Es esto lo mismo que las islas de Astro? No del todo.

En Astro, una isla es un componente real de React/Vue/Svelte, y el propio renderer de Astro para ese framework ejecuta ese componente en el servidor para producir el HTML inicial de la isla. En Nexa no hay ningún paso equivalente: el compilador de Rust jamás abre el archivo que señala data-nexa-island, ni lo ejecuta, ni lo entiende. Para Rust, una isla es literalmente una etiqueta HTML más con un atributo de texto — el mismo trato que recibe cualquier otro div.

Por eso el contenido que ves adentro del div antes de que cargue JS (el "fallback") no lo generó ningún framework: lo escribiste vos, con las mismas herramientas estáticas de cualquier página Nexa (data.*, t(), <For>). Y por eso, cuando el JS de la isla sí carga, corre 100% en el navegador — nunca en el servidor. Esta misma idea (compilador como analizador de patrones, nunca como ejecutor de tu código) es la que hace que una página normal de Nexa, sin islas, tampoco necesite "hidratarse": cada botón se activa solo, con su propio pedacito de JS — no existe un árbol completo de componentes que revivir.

Marcar la isla

<div data-nexa-island="dashboard" data-nexa-strategy="visible">
    <p>Cargando panel…</p>
</div>

El servidor solo renderiza el fallback (los hijos del div, contenido real); el montaje ocurre 100% en el cliente.

El contrato de montaje

// src/islands/dashboard.island.ts
export default function mount(el, props) {
    // @nexa/reactivity a mano, o un adaptador de Vue/Preact/etc.
    return () => { /* cleanup opcional */ };
}

Cualquier framework puede implementar esto: código escrito a mano con @nexa/reactivity, o un adaptador sobre Vue, Preact, lo que sea.

Sin reabrir la composición

// nexa.toml
[imports]
dashboard = "/vendor/dashboard-island.js"

Rust nunca abre ni interpreta el módulo del specifier — para el compilador, un data-nexa-island es un Element más, igual que cualquier otro atributo.

El hueco del medio: un modelo de componentes liviano

import { mountComponent, type Component } from "@nexa/reactivity";

interface ItemProps { label: string }
const Item: Component<ItemProps> = (el, props) => {
    el.textContent = props.label;
};

interface ListProps { labels: string[] }
const List: Component<ListProps> = (el, props, ctx) => {
    for (const label of props.labels) {
        const li = document.createElement("li");
        el.appendChild(li);
        ctx.mount(Item, li, { label });
    }
};

export default function mount(el: Element, props: Record<string, unknown>) {
    return mountComponent(List, el, { labels: props.labels as string[] });
}

Cuando una isla necesita algo más que una señal suelta pero no justifica cargar un framework externo completo (un dropdown, un widget de chat simple), @nexa/reactivity ofrece composición real — Component<P> tipado por props, mountComponent() que devuelve una función de limpieza, y un ComponentContext (onCleanup/mount) que encadena la limpieza de los hijos a la del padre automáticamente. Sigue siendo cero Virtual DOM, y exclusivamente para código dentro de una isla nativa — nunca para páginas Nexa Core. El mismo widget de panel de tareas que necesitaba Vue 3 completo (~245KB) pesa ~4KB con esto.

Usar una librería de npm de verdad, adentro de una isla

// src/islands/markdownPreview.island.ts
import { marked } from "marked"; // import normal, se bundlea con esbuild

export default function mount(el: Element, props: Record<string, unknown>) {
    const textarea = document.createElement("textarea");
    textarea.value = typeof props.initial === "string" ? props.initial : "";
    const preview = document.createElement("div");

    const render = () => { preview.innerHTML = marked.parse(textarea.value) as string; };
    textarea.addEventListener("input", render);
    el.append(textarea, preview);
    render();
}

// src/pages/index.tsx
<div
    data-nexa-island="markdownPreview"
    data-nexa-strategy="load"
    data-nexa-props={{ initial: "# Hola\n\nEsto es *Markdown* real." }}
>
    <p>Cargando editor…</p>
</div>

// nexa.toml — este [imports] es para localizar el ARCHIVO de la isla ya
// bundleado, no la librería marked (esa ya quedó adentro del bundle)
[imports]
markdownPreview = "/vendor/markdown-preview-island.js"

Ojo, esto es distinto del [imports] que viste para los handlers: un archivo de isla se bundlea completo con esbuild (--bundle --format=esm), así que un import normal en la parte de arriba del archivo se resuelve exactamente igual que en cualquier proyecto de TypeScript — sin declarar nada en nexa.toml para la librería en sí. Este ejemplo (probado de punta a punta en un navegador real) usa marked de verdad para convertir Markdown a HTML en vivo, mientras el usuario escribe.