Suspense: un fallback mientras los recursos cargan
Suspense es el límite asíncrono de Solid: envuelve un subárbol, muestra un fallback mientras cualquier recurso leído bajo él siga pendiente y revela el contenido cuando todos resuelven. La clave de su diseño es que la detección ocurre en la lectura del recurso, no en su creación: un createResource que nadie lee no suspende. Por dentro no es un condicional sino un contador de lecturas pendientes que un SuspenseContext lleva, y sus hijos no se destruyen mientras carga: se ejecutan y se retienen fuera de pantalla con su estado intacto. Gobierna el todavía no, ortogonal al ErrorBoundary que gobierna el salió mal.
El límite asíncrono y su fallback
Suspense es un componente que recibe dos cosas: un fallback —lo que se muestra mientras se espera— y sus children —el contenido real—. Colócalo alrededor de cualquier subárbol que lea uno o más recursos y Solid pintará el fallback hasta que esos recursos estén resueltos, momento en el que lo sustituye por los hijos.
import { createResource, Suspense } from "solid-js";function Perfil(props: { id: string }) { const [usuario] = createResource(() => props.id, cargarUsuario); return ( <Suspense fallback={<p>Cargando perfil…</p>}> <h1>{usuario()?.nombre}</h1> <p>{usuario()?.bio}</p> </Suspense> );}Nada aquí dice explícitamente «espera a usuario». Suspense no conoce ese recurso: se entera de que hay algo pendiente porque, al renderizar los hijos, la lectura de usuario() mientras aún carga se lo comunica. Esa es la diferencia esencial con un Show, donde tú evalúas la condición a mano; con Suspense la condición se autodescubre a partir de las lecturas que ocurren dentro. Añadir un segundo recurso al subárbol no cambia nada del código del límite: se suma solo.
La detección ocurre en la lectura
Por dentro, Suspense no es un condicional: es un contador de lecturas pendientes. El componente provee un SuspenseContext con un contador. Cada vez que se lee un recurso que todavía está cargando bajo ese límite, el recurso incrementa el contador; cuando resuelve, lo decrementa. Mientras el contador sea mayor que cero, se pinta el fallback; cuando vuelve a cero, se revelan los hijos.
La consecuencia práctica es contraintuitiva y conviene grabarla: crear un recurso no suspende; leerlo mientras carga, sí. Un createResource cuyo valor nadie lee en el árbol bajo el límite jamás activará el fallback, por muy lento que sea su fetch. Y a la inversa, un recurso leído en dos sitios distintos bajo el mismo límite cuenta una sola espera coordinada, no dos. El límite reacciona a lo que se observa, no a lo que existe.
// No suspende: el recurso se crea pero nadie lo lee bajo el Suspense.const [datos] = createResource(cargar);<Suspense fallback={<Spinner />}> <p>Contenido estático</p></Suspense>;// Sí suspende: la lectura de datos() ocurre dentro del árbol del límite.<Suspense fallback={<Spinner />}> <p>{datos()?.titulo}</p></Suspense>;El estado que mueve el contador es el mismo que expone resource.state, más rico que el booleano resource.loading:
resource.state |
Significado | loading |
|---|---|---|
unresolved |
sin origen aún, no ha empezado | no |
pending |
primera carga en curso | sí |
ready |
resuelto con valor | no |
refreshing |
revalidando con el valor previo | sí |
errored |
la promesa se rechazó | no |
Solo pending y refreshing —los dos estados en que loading es cierto— incrementan el contador del límite; leer un recurso en ready no suspende. Esta tabla es el diccionario entre lo que le pasa al recurso y lo que hace el Suspense.
Los hijos se ejecutan y se retienen
Un malentendido común es creer que Suspense no monta a sus hijos hasta que los datos llegan. Es al revés, y por una razón de diseño: para saber qué recursos esperar, el límite tiene que ejecutar a los hijos —crear sus computaciones y correr su cuerpo—, porque solo así se producen las lecturas que revelan las esperas. Suspense no aplaza la ejecución, sino que retiene la salida: los hijos corren, su DOM se construye fuera de pantalla, y el fallback ocupa su lugar hasta que todo está listo.
Esto contrasta de raíz con Show, que destruye y recrea su subárbol al cambiar la condición. Suspense mantiene una sola instancia de los hijos y solo alterna qué se muestra, así que el estado se preserva: si un recurso revalida y el límite vuelve a esperar, los signals y el DOM ya construidos siguen vivos y no se remonta desde cero.
// Show DESTRUYE y recrea el subárbol cuando la condición cambia.<Show when={!datos.loading} fallback={<Spinner />}> <Detalle datos={datos()} /></Show>;// Suspense RETIENE una sola instancia y solo alterna qué se ve.<Suspense fallback={<Spinner />}> <Detalle datos={datos()} /></Suspense>;El límite espera; el ErrorBoundary contiene
Suspense gobierna el «todavía no»: la ausencia temporal de un valor. No tiene nada que decir sobre el «salió mal»: si el fetch de un recurso se rechaza, esa rama no es competencia del límite de espera sino de un ErrorBoundary. Son ejes ortogonales y se combinan colocando el ErrorBoundary por fuera, de modo que un fallo sustituya toda la región en lugar de quedar atrapado bajo el indicador de carga.
import { createResource, Suspense, ErrorBoundary } from "solid-js";<ErrorBoundary fallback={(err) => <p>Falló: {err.message}</p>}> <Suspense fallback={<Spinner />}> <Perfil id={id()} /> </Suspense></ErrorBoundary>;El reparto es limpio: mientras el recurso carga manda Suspense y se ve el fallback; si resuelve, se ven los hijos; si se rechaza, el rechazo se reinyecta en el grafo al leer el recurso y sube hasta el ErrorBoundary. Un solo trío —recurso, Suspense, ErrorBoundary— cubre los tres estados sin un solo if manual, cada uno en su plano.