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.
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.
- Distinguir la lectura normal que suspende de la lectura
latestque no suspende. - Implementar obsoleto-mientras-revalida mostrando el valor anterior durante la recarga.
- Envolver el cambio de fuente en
useTransitionpara conservar el árbol y exponerpendiente. - 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.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.
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.
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
latesto con una transición. - Valor previo de otra entidad: bloquea keyando la frontera para no hacer pasar lo viejo por lo nuevo.
- En SSR:
deferStreamdecide si el HTML espera al dato o llega con elfallbacky 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.
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.
- Monta un buscador con
createAsyncy comprueba que sin transición cada tecleo parpadea alfallback. - Envuelve
setConsultaeniniciardeuseTransitiony verifica que la lista anterior permanece visible mientraspendiente()atenúa la interfaz. - Reescribe el mismo efecto leyendo
resultados.latestde forma explícita, sin transición, y compara ambas soluciones. - Navega entre dos perfiles distintos y decide si quieres obsoleto-mientras-revalida o bloqueo; implementa el bloqueo keyando el
Suspensepor el id. - Marca una derivación como
deferStreamy razona cómo cambia el HTML inicial que recibe el navegador frente a dejarla transmitir después.