wandres.dev
DERIVACIONES ASYNC · createAsync y el futuro

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.

⏱ 17 min

Una interfaz que parpadea a un fallback cada vez que revalida es una interfaz que castiga al usuario por pedir datos frescos. El patrón que la cura tiene nombre propio —obsoleto-mientras-revalida— y consiste en seguir mostrando el último valor bueno mientras el nuevo viaja por la red, cambiando solo cuando llega. createAsync lo soporta de dos maneras complementarias: la propiedad latest, que lee el valor anterior sin suspender, y las transiciones, que mantienen vivo el árbol entero mientras la recarga ocurre en segundo plano. Dominar ambas es la diferencia entre una app que titila y una que fluye.

🎯 Al terminar esta lección sabrás
  • Distinguir la lectura normal que suspende de la lectura latest que no suspende.
  • Implementar obsoleto-mientras-revalida mostrando el valor anterior durante la recarga.
  • Envolver el cambio de fuente en useTransition para conservar el árbol y exponer pendiente.
  • Reconocer que las navegaciones del router ya son transiciones, y cuándo bloquear a propósito.

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.latestno 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.

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.

// pinta la lista anterior mientras la busqueda nueva viaja; sin fallback, sin parpadeo
const 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.

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.

💡
El router ya envuelve cada navegación en una transición

No tienes que orquestar useTransition para la navegación entre rutas: Solid Router envuelve cada cambio de ruta en una transición por defecto. Por eso, cuando navegas a una página cuya createAsync recarga, la página anterior permanece visible sin parpadear hasta que los datos nuevos están listos, y useIsRouting te da el equivalente a pendiente para pintar una barra de progreso. El patrón obsoleto-mientras-revalida es, en la práctica, el comportamiento nativo de navegar en Solid Router; useTransition es la misma herramienta para los cambios de estado que no son navegaciones.

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.

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.

// 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 derivacion
const critico = createAsync(() => getDatosCriticos(), { deferStream: true });
stateDiagram-v2
[*] --> CargaInicial
CargaInicial --> ConValor: primera respuesta resuelve
ConValor --> Revalidando: cambia la fuente
Revalidando --> ConValor: nueva respuesta resuelve
note left of CargaInicial : lectura suspende hacia el fallback
note right of Revalidando : latest muestra lo anterior y pendiente vale verdadero
note right of ConValor : el arbol conmuta sin parpadeo
🕰️

latest

Lee el valor anterior sin suspender. Medio patron obsoleto-mientras-revalida en un punto concreto de la interfaz.

🎞️

useTransition

Conserva el arbol previo visible durante la recarga y expone pendiente para atenuar o marcar la interfaz.

🚦

Bloquear a proposito

Cuando el dato viejo es de otra entidad, keyea el Suspense para volver al fallback y no mezclar identidades.

Suspender, mostrar lo obsoleto y bloquear no son tres trucos: son tres respuestas a una sola pregunta

La pregunta que gobierna toda la experiencia asíncrona es esta: mientras el dato nuevo viaja, ¿qué merece ver el usuario? Y tiene exactamente tres respuestas, cada una correcta en su contexto, cada una encarnada en una herramienta de Solid. Si no hay nada anterior que mostrar —la primera carga— la única respuesta honesta es suspender hacia un fallback, porque no existe verdad vieja que ofrecer. Si hay un valor anterior y sigue siendo plausible —la misma búsqueda refinándose, la misma lista reordenándose— la respuesta amable es mostrar lo obsoleto mientras revalida, y para eso están latest, que te da el dato viejo en un punto, y las transiciones, que sostienen el árbol entero y te regalan un pendiente con el que susurrar “esto se está actualizando” sin gritar un spinner. Y si el valor anterior pertenece a otra entidad —cambiaste de usuario, de documento, de identidad— mostrarlo sería engañar, y la respuesta correcta vuelve a ser bloquear, keyando la frontera para que el fallback reaparezca limpio. Lo profundo es que estas tres no son técnicas rivales entre las que eliges por gusto, sino la misma decisión de diseño resuelta según una variable única: la relación entre el dato viejo y el nuevo. ¿Son la misma cosa evolucionando, o cosas distintas sustituyéndose? El framework no puede saberlo por ti porque es una cuestión de significado, no de mecánica: solo tú sabes si el perfil de 7 y el de 42 son “el perfil” cargando o dos entidades incompatibles. Solid te da las tres respuestas afiladas y te deja el juicio, que es exactamente donde debe estar. Elegir bien no es saber usar useTransition; es saber, para cada frontera de tu app, si lo que hay al otro lado del await continúa lo que ya se veía o lo reemplaza.

⚔️ Elige qué ve el usuario mientras carga
  1. Monta un buscador con createAsync y comprueba que sin transición cada tecleo parpadea al fallback.
  2. Envuelve setConsulta en iniciar de useTransition y verifica que la lista anterior permanece visible mientras pendiente() atenúa la interfaz.
  3. Reescribe el mismo efecto leyendo resultados.latest de forma explícita, sin transición, y compara ambas soluciones.
  4. Navega entre dos perfiles distintos y decide si quieres obsoleto-mientras-revalida o bloqueo; implementa el bloqueo keyando el Suspense por el id.
  5. Marca una derivación como deferStream y razona cómo cambia el HTML inicial que recibe el navegador frente a dejarla transmitir después.