Skip to content

Renderizado en el servidor (SSR)

PreffX renderiza sus componentes en el servidor a una cadena HTML y luego hidrata el mismo árbol en el cliente. El servidor emite el marcado más un script de precarga por raíz que contiene los datos asíncronos resueltos: el cliente lee ese script y alimenta el árbol con los valores en lugar de volver a obtenerlos.

Las utilidades SSR se importan desde preffx/server.

createRoot en el servidor

El servidor exporta su propio createRoot, que espeja el del cliente pero devuelve un método renderToString en lugar de mount.

ts
import { createRoot } from 'preffx/server';
import { App } from './App';

const server = createRoot();

const { preload, serialize } = server.renderToString(App, {
    props: { name: 'PreffX' }
});

await preload();          // resolve every async resource
const html = serialize(); // markup + preload data script

Normalmente se crea una raíz nueva por solicitud, porque la aplicación puede depender de la URL de la solicitud (enrutamiento, idioma) o del contexto con ámbito de solicitud:

ts
import { createRoot } from 'preffx/server';

export async function render(url: URL) {
    const root = createRoot({
        defaultURL: url,
        defaultLang: 'en',
        context: { tenant: 'acme' }
    });

    const { preload, serialize } = root.renderToString(App, {});
    await preload();

    return { appHtml: serialize() };
}

API

createRoot(config?)

Acepta las mismas opciones de raíz que la raíz del cliente, por lo que la configuración permanece sincronizada:

OpciónTipoDescripción
defaultURLstring | URLURL inicial para el enrutamiento
defaultLangstringIdioma inicial para i18n
prefixstringPrefijo por raíz (debe coincidir con la del cliente)
contextobjectContexto con ámbito de solicitud compartido con el árbol

Devuelve un objeto con un único método:

renderToString(component, config?)

  • component — el componente raíz (PC o APC).
  • config — opcional, con:
    • props — props del componente raíz.
    • serializer — función personalizada para serializar los datos precargados (por defecto JSON.stringify).

Devuelve { preload, serialize }:

MétodoFirmaDescripción
preloadpreload(timeout?)Resuelve todos los recursos asíncronos registrados. Un timeout opcional convierte un recurso lento en "best-effort": la página se renderiza igualmente y el cliente re-obtiene.
serializeserialize(): stringEmite la cadena HTML (marcado + script de datos de precarga por raíz).

Recursos en el servidor

Dentro de un componente, resource se comporta de forma transparente en el servidor: en lugar de ejecutar un efecto, registra su fetcher para preload(). Reutilice el mismo componente tanto en el servidor como en el cliente: no se necesita ninguna ramificación.

tsx
import type { PC } from 'preffx';

export const UserCard: PC = (_props, { resource, computed }) => {
    const [user] = resource(async () => {
        const res = await fetch('/api/user');
        return res.json();
    });

    const display = computed(() => user.state.value?.name ?? 'loading');

    return (
        <article className="user">
            <h1>{display}</h1>
        </article>
    );
};
  • Antes de que se ejecute preload(), user.state.value es null y pending es true, por lo que se renderiza el valor alternativo ('loading').
  • Después de preload(), el valor resuelto se incrusta en el marcado y serialize() también lo escribe en el script de datos de precarga.
  • En el cliente durante la hidratación, el recurso lee el valor precargado y omite el fetch, manteniendo pending en false.

Hidratación en el cliente

El createRoot().mount() del cliente detecta el script de datos de precarga emitido por el servidor, lo alimenta en el árbol e hidrata el DOM existente en lugar de recrearlo: no se necesita ninguna llamada adicional.

tsx
import { createRoot } from 'preffx';
import { App } from './App';

createRoot().mount(App, {
    node: document.getElementById('app')!
});

Requisitos para una hidratación correcta:

  • Las opciones del createRoot del cliente (prefix, defaultLang, defaultURL, context) deben coincidir con las del servidor.
  • El marcado renderizado debe insertarse exactamente donde el cliente lo espera (el contenedor referido por mount).

Ejemplo: flujo completo de una solicitud

tsx
// entry-server.tsx
import { createRoot } from 'preffx/server';
import { App } from './App';

export async function render(url: URL) {
    const server = createRoot({ defaultURL: url });
    const { preload, serialize } = server.renderToString(App, {});
    await preload();
    return { appHtml: serialize() };
}
tsx
// entry-client.tsx
import { createRoot } from 'preffx';
import { App } from './App';

createRoot().mount(App, { node: document.getElementById('app')! });

El servidor Node renderiza appHtml en la carcasa HTML (p. ej. index.html) y lo envía al navegador; luego el script del cliente hidrata el mismo árbol.

Publicado bajo la Licencia Apache 2.0