Saltar al contenido

useTransition y startTransition: la transición concurrente

Qué es una transición concurrente y qué problema resuelve: cuando un cambio de estado dispara trabajo asíncrono, el Suspense que envolvía el contenido detecta el recurso pendiente, desmonta lo que había y pinta su fallback, con parpadeo, pérdida de scroll y de foco. Una transición marca esa actualización como coordinación asíncrona no urgente, de modo que Solid prepara la vista nueva sin tirar la vieja y solo la compromete cuando todo lo pendiente ha resuelto. Diferencia entre useTransition, que expone un signal pending, y startTransition, que es la forma global sin indicador, y por qué esto es concurrencia cooperativa y no time-slicing.

El problema: el fallback que parpadea

Recuerda cómo se comporta <Suspense> sin transiciones. Envuelve una lectura de recurso, y cada vez que ese recurso pasa a estado pendiente —la primera carga o cualquier refetch posterior— el límite desmonta a sus hijos y muestra el fallback. Para el arranque inicial es justo lo que quieres. Para una actualización posterior es un desastre: el contenido que el usuario estaba mirando desaparece y reaparece.

TSX
import { createResource, createSignal, Suspense } from "solid-js";const [id, setId] = createSignal(1);const [usuario] = createResource(id, obtenerUsuario);function Perfil() {  return (    <Suspense fallback={<Cargando />}>      <Ficha datos={usuario()} />    </Suspense>  );}// En algún manejador de evento:setId(2); // usuario vuelve a pending -> Suspense DESMONTA Ficha -> parpadeo

Cada setId posterior repite el ciclo: recurso pendiente, fallback, contenido nuevo. El árbol se destruye y se reconstruye, y con él se van tres cosas que el usuario nota aunque no sepa nombrarlas: la posición de scroll vuelve arriba, el foco del teclado salta al body, y cualquier estado de DOM no controlado —una selección de texto, un vídeo en reproducción, una animación a medias— se pierde. El problema no es el <Suspense> —hace exactamente lo que promete—, sino que le estamos pidiendo que trate una actualización con la misma brusquedad que un arranque en frío.

La transición: coordinar en vez de desmontar

Una transición cambia ese contrato. Al envolver la escritura en startTransition, le dices a Solid que la propagación resultante es una transición: si por el camino algún recurso suspende, no muestres el fallback del <Suspense> que ya tenía contenido; retén lo viejo, computa lo nuevo aparte, y compromételo entero cuando todas las promesas hayan resuelto.

TSX
import { startTransition } from "solid-js";// La actualización se marca como transición:startTransition(() => setId(2));// La Ficha vieja permanece visible; cuando obtenerUsuario(2) resuelve,// se cambia de golpe, sin pasar por el fallback

La clave es que la transición abarca todo lo asíncrono que dispara. Si cambiar id provoca el refetch de tres recursos leídos en distintos rincones del árbol, la transición espera a los tres y los presenta a la vez.

TSX
// Un solo cambio, tres recursos que dependen de él:const [perfil] = createResource(id, obtenerPerfil);const [posts]  = createResource(id, obtenerPosts);const [amigos] = createResource(id, obtenerAmigos);// startTransition espera a los TRES antes de comprometer nada:startTransition(() => setId(2)); // perfil, posts y amigos cambian juntos

No hay ventana en la que veas parte de la vista nueva y parte de la vieja: el compromiso es atómico. Esa coordinación de múltiples fuentes asíncronas bajo una sola actualización es, literalmente, lo que la palabra transición nombra. Sin ella tendrías tres cargas independientes parpadeando a destiempo; con ella, una única conmutación limpia.

useTransition cuando necesitas el estado pending

startTransition es la forma global y sin ceremonia: no requiere dueño reactivo y devuelve una Promise que resuelve cuando la transición termina. Su hermano useTransition hace lo mismo pero además te entrega un signal de pendiente, para que la UI pueda anunciar sutilmente que hay una actualización en vuelo sin desmontar nada.

TSX
import { createSignal, useTransition } from "solid-js";function Selector() {  const [pending, start] = useTransition();  const [id, setId] = createSignal(1);  const elegir = (n: number) => start(() => setId(n));  return (    <nav aria-busy={pending()}>      <button onClick={() => elegir(1)}>Uno</button>      <button onClick={() => elegir(2)}>Dos</button>    </nav>  );}

pending() es true desde que llamas a start hasta que resuelve la última promesa coordinada. Varias llamadas a start antes de que la primera termine se funden en una única transición viva, así que el indicador refleja el estado real del conjunto, no de la última pulsación. La regla práctica: usa startTransition cuando solo quieres la coordinación, y useTransition cuando además vas a pintar un indicador de que algo se está preparando.