Saltar al contenido

latest y obsoleto-mientras-revalida: transiciones sin parpadeo

Las dos formas de leer una createAsync: la llamada normal que suspende en cada revalidación y la propiedad latest que devuelve el valor anterior sin suspender. Cómo latest implementa el patrón obsoleto-mientras-revalida mostrando datos viejos mientras llegan los nuevos, cómo useTransition mantiene el árbol anterior visible y expone un indicador de pendiente, por qué las navegaciones del router ya son transiciones por defecto, y cuándo conviene bloquear con un fallback en lugar de mostrar lo obsoleto.

Dos lecturas: la que suspende y la que no

El accessor de createAsync se puede leer de dos formas, y la elección decide la experiencia. Llamarlo —datos()— participa de Suspense: en la carga inicial, y en cada revalidación fuera de una transición, suspende hacia el fallback. Leer su propiedad —datos.latest— no suspende nunca: devuelve el último valor resuelto, incluso mientras uno nuevo está en vuelo. Es la misma dualidad que tenía createResource entre data() y data.latest, ahora en el primitivo moderno.

TSX
const resultados = createAsync(() => buscar(consulta()));// resultados()        -> suspende en la carga y en cada recarga (fuera de transicion)// resultados.latest   -> el valor anterior, sin suspender, mientras revalida

La lectura latest es, por sí sola, medio patrón obsoleto-mientras-revalida: te da el dato viejo para pintar sin interrupción mientras el nuevo llega. La usas cuando quieres controlar tú, a mano, qué mostrar durante la recarga en un punto concreto de la interfaz.

TSX
// pinta la lista anterior mientras la busqueda nueva viaja; sin fallback, sin parpadeoconst lista = () => resultados.latest ?? [];return <For each={lista()}>{(fila) => <Fila dato={fila} />}</For>;

Transiciones: mantener el árbol vivo

latest resuelve un punto; las transiciones resuelven el árbol entero. useTransition te da un par: un accessor pendiente y una función iniciar. Envuelves en iniciar el cambio de estado que dispara la revalidación, y Solid mantiene el render anterior visible —sin suspender— mientras la nueva versión se prepara en segundo plano. Cuando el dato nuevo resuelve, el árbol conmuta de golpe; entre tanto, pendiente() vale verdadero y te deja atenuar o marcar la interfaz.

TSX
import { createSignal, useTransition } from "solid-js";import { createAsync } from "@solidjs/router";const [consulta, setConsulta] = createSignal("");const [pendiente, iniciar] = useTransition();const resultados = createAsync(() => buscar(consulta()));function alBuscar(texto: string) {  // la transicion conserva la lista vieja visible mientras llega la nueva  iniciar(() => setConsulta(texto));}return (  <div classList={{ atenuado: pendiente() }}>    <Suspense fallback={<Spinner />}>      <For each={resultados()}>{(fila) => <Fila dato={fila} />}</For>    </Suspense>  </div>);

Fíjate en que dentro de la transición lees resultados() con normalidad —la forma que suspende— y aun así no ves el fallback en las recargas: la transición intercepta la suspensión y sostiene el árbol previo. El Suspense solo actúa en la primera carga, cuando no hay ningún valor anterior que mostrar. Transición y latest no compiten: la transición gestiona el árbol y te da el pendiente; latest te da el valor concreto por si quieres pintar algo obsoleto de forma explícita.

Etiquetar lo obsoleto con honestidad

Mostrar datos viejos es amable, pero se vuelve engañoso si el usuario no sabe que mira algo que ya se está actualizando. El patrón honesto combina las dos lecturas: pintas el valor sostenido para no dejar la pantalla en blanco y usas pendiente para señalar que lo mostrado es provisional. Un ligero atenuado, un rótulo de actualizando o un aria-busy bastan para que el obsoleto informe en vez de mentir.

TSX
const [pendiente, iniciar] = useTransition();const resultados = createAsync(() => buscar(consulta()));return (  <div classList={{ obsoleto: pendiente() }} aria-busy={pendiente()}>    <For each={resultados.latest ?? []}>{(fila) => <Fila dato={fila} />}</For>    <Show when={pendiente()}><span class="aviso">Actualizando…</span></Show>  </div>);

Esa combinación separa una interfaz obsoleta-pero-honesta de una que finge estar al día: el valor sostenido evita el hueco, pendiente confiesa que hay algo mejor en camino. El usuario nunca ve un vacío ni un dato que se hace pasar por fresco sin serlo, que es la única forma de que mostrar lo viejo sume confianza en vez de restarla.

Cuándo mostrar lo obsoleto y cuándo bloquear

Obsoleto-mientras-revalida es casi siempre lo correcto, pero no siempre. Mostrar datos viejos es honesto cuando siguen siendo plausibles —resultados de una búsqueda que se refina, una lista que se reordena—. Deja de serlo cuando el dato viejo pertenece a otra entidad: si navegas del usuario 7 al usuario 42, sostener el perfil de 7 mientras carga 42 no es suave, es mentir. Ahí quieres bloquear con el fallback para no mezclar identidades.

Hay tres respuestas, y una sola variable decide cuál:

  • Sin valor previo (primera carga): suspende hacia el fallback, porque no hay verdad vieja que ofrecer.
  • Valor previo plausible (la misma entidad evolucionando): muestra lo obsoleto con latest o con una transición.
  • Valor previo de otra entidad: bloquea keyando la frontera para no hacer pasar lo viejo por lo nuevo.
  • En SSR: deferStream decide si el HTML espera al dato o llega con el fallback y lo transmite después.

La palanca es la key del Suspense, o partir el árbol para que la frontera envuelva solo lo que sí puede quedarse obsoleto. En el servidor, además, deferStream decide si el flujo de SSR espera a que la derivación resuelva antes de enviar, u ofrece el fallback en el HTML inicial y transmite el dato después.

TSX
// bloquea a proposito al cambiar de identidad: nuevo id, nuevo Suspense, fallback limpio<Suspense fallback={<Spinner />}>  <Show keyed when={props.id}>    {(id) => <PerfilDe id={id} />}  </Show></Suspense>// en SSR: espera al dato antes de enviar el HTML de esta derivacionconst critico = createAsync(() => getDatosCriticos(), { deferStream: true });