Saltar al contenido

Pantalla real: listado y detalle maestro-detalle

Integrar en una sola pantalla todo el nivel: dos rutas anidadas —lista y detalle— donde `query` da identidad a los datos, `preload` los calienta en hover, `createAsync` los lee, `Suspense` marca las fronteras de carga y las transiciones del router mantienen visible el detalle anterior mientras el nuevo llega. Cómo `useIsRouting` pinta un indicador de navegación sin parpadeos, por qué bajo transición un `Suspense` ya resuelto no vuelve al fallback, y cómo `useSubmission` cierra el bucle con una mutación optimista que revalida por clave.

Anatomía de la pantalla: dos rutas anidadas

El maestro-detalle se modela como una ruta de layout que pinta la lista y aloja un hueco para el detalle, más una ruta hija que rellena ese hueco según el id de la URL. El layout lee la lista una vez; cada hijo lee su producto. Ambas rutas declaran preload para calentar la caché antes de renderizar.

TSX
// ~/routes/productos.tsx  (layout: lista + hueco de detalle)import type { RouteDefinition } from "@solidjs/router";import { createAsync, useIsRouting, A } from "@solidjs/router";import { Suspense, For } from "solid-js";import { getProductos } from "~/lib/productos";export const route = {  preload: () => void getProductos(),} satisfies RouteDefinition;export default function ProductosLayout(props) {  const productos = createAsync(() => getProductos());  const navegando = useIsRouting();  return (    <div class="maestro-detalle" classList={{ navegando: navegando() }}>      <aside>        <Suspense fallback={<EsqueletoLista />}>          <For each={productos()}>            {(p) => <A href={`/productos/${p.id}`}>{p.nombre}</A>}          </For>        </Suspense>      </aside>      <main>{props.children}</main>    </div>  );}

Cablear el detalle: query, preload y createAsync

La ruta hija lee su producto por el id de la URL. preload dispara la query —y de paso las reseñas diferibles— sin esperarlas; createAsync las relee y encuentra la caché caliente. Los enlaces A de la lista, al hacer hover, ya habrán ejecutado este preload, así que al hacer clic el dato suele estar listo.

TSX
// ~/routes/productos/[id].tsx  (detalle)import type { RouteDefinition } from "@solidjs/router";import { createAsync, useParams } from "@solidjs/router";import { Suspense } from "solid-js";import { getProducto, getResenas } from "~/lib/productos";export const route = {  preload({ params }) {    void getProducto(params.id);      // critico    void getResenas(params.id);       // diferido: streameara  },} satisfies RouteDefinition;export default function Detalle() {  const params = useParams();  const producto = createAsync(() => getProducto(params.id), { deferStream: true });  const resenas = createAsync(() => getResenas(params.id));  return (    <Suspense fallback={<EsqueletoDetalle />}>      <h1>{producto()?.nombre}</h1>      <p>{producto()?.precio}</p>      <Suspense fallback={<EsqueletoResenas />}>        <ListaResenas resenas={resenas()} />      </Suspense>    </Suspense>  );}

Suspense y transiciones: navegar sin parpadeos

Aquí ocurre la magia que hace que la pantalla se sienta fluida. Las navegaciones del router son transiciones, y eso tiene dos consecuencias que colaboran. Primera: createAsync conserva el valor anterior mientras el nuevo carga, en vez de volver a undefined. Segunda: bajo una transición, un Suspense que ya resolvió no vuelve a su fallback —mantiene el contenido viejo hasta que el nuevo está listo y entonces lo sustituye—. El resultado es que al saltar del producto 7 al 8, el detalle del 7 permanece nítido mientras el 8 llega, sin un parpadeo al esqueleto.

El fallback del Suspense, por tanto, solo se ve en la primera carga, cuando aún no había nada que conservar. Para señalar las navegaciones siguientes se usa useIsRouting, que devuelve un accessor booleano de si hay una transición en curso; con él pintas una barra de progreso o atenúas el panel, sin tocar el contenido.

TSX
const navegando = useIsRouting();// clase que atenua el detalle mientras se navega, sin quitar el contenido viejo<main classList={{ cargando: navegando() }}>{props.children}</main>

Cerrar el bucle: mutar y revalidar en la misma pantalla

Una pantalla real no solo lee: escribe. Publicar una reseña es una action que, al terminar, revalida solo las reseñas de ese producto con keyFor. Mientras la petición vuela, useSubmission expone su estado pendiente y sus datos enviados, con los que pintas la reseña optimista —proyectas el futuro antes de que el servidor confirme—; cuando la revalidación trae la verdad, la fila fantasma cede su sitio a la real.

TSX
import { action, json, useSubmission } from "@solidjs/router";import { getResenas } from "~/lib/productos";export const publicarResena = action(async (form: FormData) => {  "use server";  const id = form.get("productoId") as string;  await db.resena.create({ data: leer(form) });  return json({ ok: true }, { revalidate: getResenas.keyFor(id) }); // solo estas resenas}, "publicarResena");function FormResena(props: { productoId: string }) {  const envio = useSubmission(publicarResena);  return (    <form action={publicarResena} method="post">      <input type="hidden" name="productoId" value={props.productoId} />      <textarea name="texto" />      <button disabled={envio.pending}>        {envio.pending ? "publicando..." : "publicar"}      </button>    </form>  );}

Con esto la pantalla respira sola: la lista se calienta en hover, el detalle cambia sin parpadeos, las reseñas diferidas se transmiten por streaming, y una nueva reseña aparece al instante y se reconcilia con el servidor bajo su clave. Cada primitiva del nivel ha aportado su parte, y ninguna sobra.