wandres.dev
TRANSITIONS · startTransition, useTransition

Transiciones en navegación, pestañas y filtros

Dónde viven las transiciones en una aplicación real: el router de Solid envuelve cada navegación en una transición por defecto, de modo que la página vieja permanece visible mientras los datos de la ruta nueva se cargan, y useIsRouting expone el pending de esa navegación para una barra de progreso global. Del lado del estado local, los cambios de pestaña o de filtro que alimentan la fuente de un createResource deben envolverse en startTransition para heredar el mismo comportamiento sin parpadeo. Cómo encajan createAsync y las funciones de precarga que calientan la caché para que la transición sea instantánea.

⏱ 17 min

Las transiciones no son un adorno que se aplica a mano de vez en cuando: son la columna vertebral de dos de las interacciones más frecuentes de cualquier aplicación, navegar entre rutas y refinar una vista con pestañas o filtros. El router de Solid ya envuelve cada navegación en una transición sin que hagas nada, así que la página anterior no parpadea mientras la nueva carga sus datos. Y del lado del estado local, cualquier cambio que altere la fuente de un recurso hereda el mismo cero parpadeo con solo envolverlo en startTransition. Esta lección conecta la primitiva con los sitios donde de verdad la vas a usar.

🎯 Al terminar esta lección sabrás
  • Saber que @solidjs/router envuelve cada navegación en una transición por defecto.
  • Usar useIsRouting para una barra de progreso global de navegación.
  • Envolver cambios de pestaña o filtro que alimentan un recurso en startTransition.
  • Coordinar createAsync y las funciones de precarga con el modelo de transiciones.

El router ya usa transiciones

Cuando navegas con @solidjs/router, el cambio de ruta ocurre dentro de una transición de forma automática. Si la ruta destino lee datos que aún se están buscando —con createAsync, con una función load, o con cualquier recurso—, el router mantiene la ruta actual en pantalla hasta que esos datos resuelven, y entonces conmuta de golpe. No tienes que envolver nada: la navegación es la transición. Esto explica por qué, en una app con router bien montada, pasar de una página a otra no parpadea aunque cada página cargue lo suyo.

import { createAsync, useParams } from "@solidjs/router";

function Articulo() {
  const params = useParams();
  const articulo = createAsync(() => obtenerArticulo(params.id));
  // Al navegar aquí, la página anterior permanece visible
  // mientras esta promesa resuelve; luego se conmuta entera
  return (
    <Suspense fallback={<Skeleton />}>
      <Vista datos={articulo()} />
    </Suspense>
  );
}

Esto reordena una intuición que muchos traen de otros frameworks, donde navegar significa desmontar la pantalla actual y montar la siguiente con su propio spinner. En Solid la pantalla actual es una verdad que se conserva hasta que la siguiente está lista, exactamente igual que en cualquier otra transición. La navegación no es un caso especial: es la aplicación más visible del mismo mecanismo que ya conoces.

useIsRouting: el pending de la navegación

Igual que useTransition te da un pending local, el router te da useIsRouting: un signal booleano que es true mientras una navegación está en vuelo. Es la pieza para una barra de progreso global —esa línea fina que corre por el borde superior de tantas aplicaciones— sin acoplarla a ninguna ruta concreta.

import { useIsRouting } from "@solidjs/router";

function ProgresoNavegacion() {
  const enRuta = useIsRouting();
  return (
    <div
      class="barra-navegacion"
      classList={{ activa: enRuta() }}
      aria-hidden="true"
    />
  );
}

Colócalo una sola vez, alto en el árbol, fuera del <Routes>, y anunciará cualquier navegación de la aplicación. Es la contrapartida global de los indicadores sutiles de la lección anterior: mismo principio —anotar, no reemplazar—, pero para el nivel de la ruta entera en vez del de un bloque local. Y como la página vieja sigue montada, el usuario puede seguir leyéndola —o incluso cancelar la navegación yendo a otro sitio— mientras la línea corre.

flowchart LR
N[clic en enlace] -->|router envuelve en transicion| L[carga datos de la ruta nueva]
L -->|useIsRouting true| P[pagina vieja sigue visible]
P -->|datos listos| C[commit de la ruta nueva]
style P fill:#f9e2af,color:#11111b
style C fill:#a6e3a1,color:#11111b

Filtros y pestañas: transiciones que tú disparas

El router te da las transiciones gratis en la navegación, pero muchos cambios de vista no cruzan rutas: refinar un catálogo con un filtro, cambiar de pestaña, reordenar una tabla. Si esos controles alteran un signal que es la fuente de un createResource, el recurso refetchea y —sin transición— el <Suspense> parpadea. La solución es la misma primitiva, disparada a mano: envuelve la escritura del signal en startTransition.

import { createResource, createSignal, startTransition, Suspense } from "solid-js";

function Catalogo() {
  const [filtro, setFiltro] = createSignal("todos");
  const [productos] = createResource(filtro, buscarProductos);

  const cambiar = (f: string) => startTransition(() => setFiltro(f));

  return (
    <>
      <Filtros onPick={cambiar} />
      <Suspense fallback={<Skeleton />}>
        <Rejilla items={productos()} />
      </Suspense>
    </>
  );
}

Si además quieres atenuar la rejilla mientras el filtro nuevo carga, cambia startTransition por useTransition y usa su pending:

function CatalogoAtenuado() {
  const [filtro, setFiltro] = createSignal("todos");
  const [pending, start] = useTransition();
  const [productos] = createResource(filtro, buscarProductos);

  return (
    <>
      <Filtros onPick={(f) => start(() => setFiltro(f))} />
      <Suspense fallback={<Skeleton />}>
        <Rejilla items={productos()} classList={{ atenuado: pending() }} />
      </Suspense>
    </>
  );
}

El patrón se repite idéntico para pestañas —el signal es la pestaña activa— y para ordenación —el signal es el criterio—. La regla mental es simple: cualquier signal que sea fuente de un recurso y lo cambie el usuario merece un startTransition. Ese es el punto exacto donde una interfaz pasa de parpadear a fluir.

🧭

Navegación: gratis

El router envuelve cada cambio de ruta en una transición. La página vieja aguanta hasta que la nueva tiene datos.

📊

useIsRouting: global

El pending de la navegación entera, para una barra de progreso alta en el árbol, desacoplada de cada ruta.

🎛️

Filtros: a mano

Un signal fuente de un recurso que cambia el usuario merece startTransition para heredar el cero parpadeo.

ℹ️
Precarga: calienta la caché para que la transición sea instantánea

Una transición espera a que los datos lleguen; si además los pides antes de que el usuario actúe, la espera se acorta o desaparece. El router de Solid ejecuta las funciones preload al pasar el ratón o enfocar un enlace, de modo que cuando el clic llega la promesa ya está resuelta o casi, y la caché de query la sirve al instante. Transición y precarga son socios naturales: la primera oculta la latencia que quede, la segunda reduce esa latencia de entrada.

import { query } from "@solidjs/router";

const obtenerArticulo = query(async (id: string) => {
  return fetch(`/api/articulo/${id}`).then((r) => r.json());
}, "articulo");

// En la definición de la ruta: se dispara al hover/focus del enlace
// preload: ({ params }) => obtenerArticulo(params.id)
La misma primitiva, dos puertas de entrada: automática en la ruta, manual en el estado

La lección estratégica de este nivel aplicado es que las transiciones tienen exactamente dos puertas de entrada en una aplicación, y reconocerlas te dice dónde poner tu esfuerzo. La primera puerta es automática y ya está abierta: la navegación entre rutas, que el router envuelve en una transición sin pedirte permiso, porque la navegación es el caso arquetípico de reemplazar una vista entera cuyos datos tardan. Ahí tu único trabajo es leer el pending con useIsRouting si quieres anunciarlo, y calentar la caché con precarga para que la latencia que la transición oculta sea, además, corta. La segunda puerta es manual y la abres tú: cada cambio de estado local que altera la fuente de un recurso —el filtro, la pestaña, el criterio de orden, la página—. Esos cambios no cruzan rutas, así que el router no los ve, y sin un startTransition explícito recaen en el comportamiento por defecto del <Suspense>, que es parpadear. La belleza es que ambas puertas dan al mismo mecanismo: no hay dos sistemas que aprender, hay uno solo que unas veces se dispara por ti y otras lo disparas tú. Una vez que ves esa unidad, auditar una aplicación se vuelve mecánico: recorres cada punto donde un recurso cambia de fuente por acción del usuario y te preguntas si esa transición existe. Donde el router navega, existe; donde un signal local muta una fuente, tienes que ponerla. Esa auditoría —tan simple como listar los orígenes de refetch y verificar que cada uno está envuelto— es la diferencia entre una app que parpadea a saltos y una que se desliza de un estado a otro sin una sola sacudida.

⚔️ Envuelve toda la app
  1. Coloca un ProgresoNavegacion con useIsRouting alto en el árbol y navega entre rutas con datos para verlo encenderse y apagarse.
  2. Confirma que, al navegar a una ruta con createAsync pendiente, la página anterior permanece visible sin fallback intermedio.
  3. Monta un catálogo con filtro sobre un createResource y observa el parpadeo al cambiar de filtro sin transición.
  4. Envuelve el cambio de filtro en startTransition y verifica que el parpadeo desaparece; luego pasa a useTransition para atenuar la rejilla mientras carga.
  5. Añade una función preload al enlace o al filtro y mide cuánto se acorta la espera de la transición cuando la caché ya está caliente.