wandres.dev
SUSPENSE · coordinar la carga

SuspenseList: orquestar el orden de revelado

Cuando varios Suspense hermanos resuelven a tiempos impredecibles, el contenido aparece en desorden y la página salta. SuspenseList impone un orden de revelado sobre esos límites —forwards de arriba abajo, backwards de abajo arriba, together todo a la vez— con independencia de quién termine primero, y tail controla cómo se muestran los fallback de lo que aún no toca aparecer: collapsed solo el siguiente, hidden ninguno, o todos por defecto. Ordena la aparición, no la carga: los recursos siguen cargando en paralelo. Se puede anidar para coordinar bloques dentro de bloques.

⏱ 16 min

Una lista donde cada fila carga sus datos por separado tiene un problema estético serio: las filas aparecen en el orden en que terminan sus peticiones, que es aleatorio, así que el contenido «palomea» y el layout salta mientras los huecos se rellenan a destiempo. SuspenseList es la respuesta de Solid: envuelve a varios Suspense hermanos y les impone un orden de revelado —de arriba abajo, de abajo arriba o todo junto— sin tocar cuándo cargan. Con tail decides además cuántos indicadores de espera se ven a la vez. Es control de coreografía puro: separa el orden en que las cosas llegan del orden en que el usuario las ve aparecer.

🎯 Al terminar esta lección sabrás
  • Envolver varios Suspense hermanos en un SuspenseList para coordinar su revelado.
  • Elegir revealOrder entre forwards, backwards y together.
  • Controlar con tail si los pendientes se muestran collapsed, hidden o todos.
  • Anidar SuspenseList para coordinar bloques dentro de bloques.

Qué coordina SuspenseList

SuspenseList no carga nada ni suspende por sí mismo: es un coordinador que observa a sus Suspense hijos directos (y a otros SuspenseList anidados) y decide en qué secuencia se les permite revelar su contenido. Los recursos de cada límite siguen disparándose en paralelo desde el primer momento; lo que SuspenseList intercepta es el instante de aparición, reteniendo a un límite ya resuelto si su turno todavía no llegó según el orden que pediste.

Sin un SuspenseList, tres Suspense hermanos revelan cada uno en cuanto su propio recurso resuelve, en el orden impredecible de la red:

// Sin coordinar: cada límite aparece cuando quiere; el orden lo decide el azar.
<>
  <Suspense fallback={<Fila />}><Articulo id="a" /></Suspense>
  <Suspense fallback={<Fila />}><Articulo id="b" /></Suspense>
  <Suspense fallback={<Fila />}><Articulo id="c" /></Suspense>
</>;

Envolverlos en un SuspenseList es todo lo que hace falta para imponerles una secuencia:

import { Suspense, SuspenseList } from "solid-js";

<SuspenseList revealOrder="forwards" tail="collapsed">
  <Suspense fallback={<Fila />}><Articulo id="a" /></Suspense>
  <Suspense fallback={<Fila />}><Articulo id="b" /></Suspense>
  <Suspense fallback={<Fila />}><Articulo id="c" /></Suspense>
</SuspenseList>;

Aunque el artículo b termine su carga antes que a, con revealOrder="forwards" no se mostrará hasta que a haya aparecido. El usuario ve las filas llenarse en orden natural, de arriba abajo, como si cargaran secuencialmente, aunque por dentro corrieran todas a la vez. Ganas la lectura ordenada sin pagar la latencia de cargar en fila.

revealOrder: forwards, backwards, together

revealOrder admite tres valores. forwards revela en el orden del documento: cada límite espera a que el anterior aparezca, aunque él estuviera listo antes. backwards hace lo mismo en sentido inverso, de abajo arriba —útil para historiales o chats donde lo último ancla la vista—. together retiene a todos hasta que el último resuelve y entonces los revela de golpe, convirtiendo a muchos límites en un único momento de aparición coordinado.

flowchart LR
A[a listo] --> R[revela a]
R --> RB[revela b]
RB --> RC[revela c]
B[b listo antes que a] -.espera su turno.-> RB
style R fill:#a6e3a1,color:#11111b
style RB fill:#a6e3a1,color:#11111b
revealOrder Orden de aparición Uso típico
forwards de arriba abajo, en orden de documento feeds y listas
backwards de abajo arriba chats e historiales anclados
together todos a la vez, al resolver el último rejillas y unidades visuales

La elección comunica intención. forwards es el orden de lectura por defecto de casi cualquier feed o lista occidental. together sirve cuando las piezas forman una unidad visual y verlas aparecer por separado rompería la composición —una rejilla de tarjetas que debe cuajar entera—. Y backwards es el idioma de las interfaces ancladas abajo. En los tres casos, el reparto de revelado es una decisión de diseño explícita, no el azar de qué petición ganó la carrera.

tail: cómo se muestran los pendientes

Ordenar el revelado deja una pregunta abierta: mientras esperas a que llegue el turno de cada límite, ¿cuántos fallback se ven? Eso lo gobierna tail. Por defecto —sin tail— se muestran todos los fallback de los límites que aún no han revelado, así que ves la lista completa de esqueletos desde el principio. Con tail="collapsed" se muestra solo el fallback del siguiente en aparecer, ocultando el resto. Con tail="hidden" no se muestra ningún fallback de los pendientes: el contenido simplemente crece a medida que se revela.

// Solo un indicador a la vez, el del próximo en aparecer.
<SuspenseList revealOrder="forwards" tail="collapsed">
  {items.map((it) => (
    <Suspense fallback={<FilaEsqueleto />}>
      <Fila datos={it} />
    </Suspense>
  ))}
</SuspenseList>;
tail Fallbacks visibles de lo pendiente
sin valor (por defecto) todos los esqueletos a la vez
collapsed solo el del siguiente en aparecer
hidden ninguno

collapsed es la opción idónea para listas largas o scroll infinito: en vez de renderizar cientos de esqueletos, muestras uno que anuncia la continuación. hidden encaja cuando ni siquiera quieres insinuar la espera. Y el modo por defecto —todos visibles— conviene a listas cortas de tamaño conocido, donde ver el molde completo desde el inicio evita el salto de layout. revealOrder decide la secuencia; tail decide el ruido de esa secuencia.

Anidar SuspenseList

Un SuspenseList puede contener a otro, y ahí está su verdadera potencia composicional: la lista externa trata a la interna como una sola unidad de revelado. Así coordinas la aparición de bloques enteros y, dentro de cada bloque, la de sus elementos, con políticas distintas en cada nivel.

<SuspenseList revealOrder="forwards">
  <ProfileHeader />
  <SuspenseList revealOrder="together">
    <Suspense fallback={<Tarjeta />}><Foto /></Suspense>
    <Suspense fallback={<Tarjeta />}><Bio /></Suspense>
  </SuspenseList>
  <SuspenseList revealOrder="forwards" tail="collapsed">
    {posts.map((p) => (
      <Suspense fallback={<FilaEsqueleto />}><Post datos={p} /></Suspense>
    ))}
  </SuspenseList>
</SuspenseList>;

La cabecera aparece primero; después el bloque foto-más-bio cuaja de golpe con su together; y por último el feed se revela fila a fila con un solo esqueleto a la vista. El árbol de listas te da un lenguaje jerárquico para orquestar toda la aparición de una página compleja, cada rama con su ritmo, sin que ninguna deje de cargar en paralelo por debajo.

⬇️

forwards / backwards

Revela en orden de documento o al revés; un límite espera su turno aunque ya esté listo.

🎬

together

Retiene a todos hasta que el último resuelve y los muestra en un único instante.

🤏

tail collapsed / hidden

Un solo fallback (el siguiente) o ninguno, en vez de la pared de esqueletos por defecto.

💡
Solo coordina a sus Suspense hijos directos

SuspenseList gobierna a los Suspense (y SuspenseList) que son sus hijos directos, no a límites enterrados dentro de otros componentes. Si envuelves cada fila en un componente que a su vez contiene su Suspense, ese límite sigue siendo hijo directo de la lista siempre que el componente lo devuelva en la posición del hijo; pero si lo escondes bajo capas de div o lo mueves a un Portal, se sale de la coordinación. La regla práctica: mantén los Suspense que quieras ordenar como hijos inmediatos del SuspenseList, ya sea directamente o a través de componentes que los devuelvan en su raíz. Lo que la lista no ve, no lo ordena.

📝
Ordena el revelado, nunca la carga

El error de modelo mental con SuspenseList es creer que serializa las peticiones. No lo hace: todos los recursos de todos los límites arrancan en paralelo en cuanto se montan, igual que sin SuspenseList. Lo único que se retiene es el momento visual en que un límite ya resuelto muestra su contenido. Por eso forwards no penaliza la latencia total —la lista tarda lo que el más lento, no la suma—; solo reordena la aparición para que sea legible. Si de verdad quisieras cargar en secuencia, eso lo haría una cascada de orígenes, no SuspenseList, que es exactamente lo contrario: máxima concurrencia de carga con máximo orden de revelado.

ℹ️
Dónde brilla y dónde sobra

No necesitas SuspenseList para un único Suspense; se vuelve indispensable en cuanto tienes varios hermanos cuyo orden de aparición importa para la legibilidad: listas largas, feeds, tableros con muchas tarjetas asíncronas. La relación con Suspense es de escala: donde un Suspense coordina la espera de varios recursos en un punto, un SuspenseList coordina la aparición de varios puntos en una secuencia. Uno resuelve el «esperamos juntos»; el otro, el «aparecemos en orden». Reserva la lista para cuando el desorden de aparición sea de verdad un problema visual, no como envoltorio reflejo de cualquier grupo de límites.

Separar el orden de llegada del orden de aparición

La idea profunda de SuspenseList es que en una interfaz asíncrona hay dos órdenes distintos que casi todo el mundo confunde en uno solo: el orden en que los datos llegan y el orden en que el usuario los ve aparecer. Sin coordinación, ambos coinciden por defecto, y como el primero es el azar de la red —qué petición resolvió antes—, el segundo hereda ese caos: filas que palomean, layout que salta, una experiencia que se siente rota aunque cada pieza funcione. SuspenseList rompe esa identidad forzada. Deja que la llegada siga siendo lo que es —paralela, impredecible, óptima en tiempo— y le superpone un orden de aparición que tú diseñas: de arriba abajo porque así se lee, todo junto porque forman una unidad, de abajo arriba porque la vista está anclada ahí. Que un límite ya resuelto espere pacientemente su turno no es desperdiciar su carga; es cobrar en legibilidad lo que la red te dio en concurrencia. Y tail extiende el mismo principio al ruido de la espera: no solo controlas en qué orden aparece lo listo, sino cuánto de lo no-listo se insinúa mientras tanto. Cuando interiorizas que carga y revelado son ejes independientes, dejas de aceptar el desorden como precio de lo asíncrono y empiezas a coreografiarlo: toda la velocidad del paralelo, toda la calma de lo secuencial, sin renunciar a ninguna.

⚔️ Coreografía una lista asíncrona
  1. Monta tres Suspense hermanos con latencias deliberadamente desordenadas y observa sin SuspenseList cómo aparecen en orden de terminación.
  2. Envuélvelos en un SuspenseList con revealOrder="forwards" y confirma que ahora aparecen de arriba abajo aunque el segundo cargue primero.
  3. Cambia a together y verifica que ninguno se muestra hasta que el más lento resuelve, y entonces aparecen todos de golpe.
  4. Prueba tail="collapsed" en una lista de veinte elementos y comprueba que solo se ve un esqueleto —el del siguiente— en lugar de veinte.
  5. Anida un SuspenseList con together dentro de otro con forwards y describe cómo el externo trata al interno como una sola unidad de revelado.