Los estados del resource: loading, error, latest y state
Un resource es más que su valor: el accessor lleva su ciclo de vida encima en cuatro propiedades reactivas. loading dice si hay una petición en vuelo; error captura el rechazo del fetcher; state condensa cinco fases —unresolved, pending, ready, refreshing y errored— con más resolución que un booleano; y latest devuelve el último valor conocido sin suspender, la clave para interfaces sin parpadeo mientras una recarga viaja. Leerlas reactivamente en el JSX es modelar la carga como un pequeño autómata declarativo.
Un resource entrega un valor, pero entrega mucho más que eso. La operación asíncrona que envuelve tiene un ciclo de vida —empieza, viaja, resuelve o falla, quizá se recarga— y ese ciclo es información que tu interfaz necesita: un spinner mientras carga, un mensaje si falla, los datos anteriores mientras llega un refresco. Solid no te obliga a derivar ese estado a mano: lo cuelga del propio accessor en cuatro propiedades reactivas —loading, error, latest y state—. Esta lección las diseca una a una y muestra cómo componerlas en el JSX, hasta que leer un resource sea leer, de un vistazo, todo su ciclo de vida.
- Leer
loadingyerrorcomo propiedades reactivas del accessor, no como valores sueltos. - Distinguir los cinco valores de
state:unresolved,pending,ready,refreshingyerrored. - Entender por qué
latestdevuelve el valor anterior sin suspender, ideal para UIs sin parpadeo. - Componer estos estados en el JSX para cubrir carga, error y recarga con precisión.
El accessor lleva su ciclo de vida encima
El accessor que devuelve createResource no es una función pelada: es una función con propiedades adosadas. Junto al valor, expone loading, error, latest y state, y cada una es reactiva —leerlas crea una suscripción, igual que llamar al accessor—.
const [usuario] = createResource(id, traerUsuario);
usuario(); // el valor: Usuario | undefined
usuario.loading; // boolean: hay una peticion en vuelo
usuario.error; // el error si el fetcher rechazo, undefined si no
usuario.latest; // el ultimo valor conocido, sin suspender
usuario.state; // "unresolved" | "pending" | "ready" | "refreshing" | "errored"
Que sean reactivas es lo que las hace útiles: no son una foto del momento, sino señales vivas que el grafo actualiza en cada transición del ciclo. Puedes cablearlas directamente al JSX y dejar que la interfaz siga el estado de la petición sin que tú muevas un dedo.
Un apunte de grano fino: como cada propiedad se rastrea por separado, un componente que solo lee usuario.loading se re-evalúa cuando cambia la carga, pero no cuando cambia el valor, y a la inversa. No pagas por leer más de lo que usas. Esa precisión, heredada de todo el modelo reactivo de Solid, hace que exponer cuatro facetas en un mismo accessor no tenga coste oculto: cada consumidor se suscribe solo a la que le importa.
Una nota sobre error: su tipo es unknown —o any según tu configuración—, porque un fetcher puede rechazar con cualquier cosa, no solo con un Error. Trátalo con la misma cautela que un catch: estrecha su tipo antes de leer un .message, y no des por hecha la forma de algo que vino de la red y podría ser una cadena, un objeto o nada reconocible.
function Ficha() {
const [usuario] = createResource(id, traerUsuario);
return (
<Switch fallback={<Perfil datos={usuario()!} />}>
<Match when={usuario.loading}>
<Spinner />
</Match>
<Match when={usuario.error}>
<Aviso mensaje="No se pudo cargar" detalle={usuario.error} />
</Match>
</Switch>
);
}
Fíjate en lo que no hay: ni un isLoading declarado a mano, ni un error que sincronizar en un catch, ni un efecto que ate uno a otro. Lees las propiedades del resource directamente en el Switch, y la interfaz sigue el ciclo de vida de la petición porque esas propiedades son señales vivas. El componente deja de gestionar el estado de carga y pasa a ser una proyección de él: una función del estado del resource al árbol que se ve.
Los cinco estados de state
loading y error cuentan lo esencial, pero state cuenta lo mismo con más resolución: un solo valor que nombra la fase exacta del ciclo.
unresolved— no ha habido petición aún; la fuente es falsy. El valor esundefined.pending— primera carga en vuelo, todavía sin ningún valor que mostrar. El valor esundefined.ready— la promesa resolvió; hay valor.refreshing— hay un valor previo y se está recargando (porrefetcho por cambio de fuente).errored— el fetcher rechazó;errorestá poblado.
La distinción que más rendimiento te dará es la que hay entre pending y refreshing. En ambas loading vale true —hay una petición viajando—, pero pending significa «cargando y no tengo nada que enseñar», mientras que refreshing significa «cargando, pero conservo el valor anterior». Esa diferencia es la que te deja elegir entre un spinner a pantalla completa y un refresco discreto que no borra lo que el usuario ya está viendo.
flowchart TD U[unresolved sin fuente] -->|fuente truthy| P[pending primera carga] P -->|resuelve| RD[ready con valor] P -->|rechaza| ER[errored] RD -->|refetch o cambio de fuente| RF[refreshing conserva valor] RF -->|resuelve| RD RF -->|rechaza| ER ER -->|refetch| P style P fill:#89b4fa,color:#11111b style RD fill:#a6e3a1,color:#11111b style RF fill:#f9e2af,color:#11111b style ER fill:#f38ba8,color:#11111b
Con este mapa, loading deja de ser un misterio: es exactamente la disyunción de los dos estados en vuelo, y podrías derivarlo tú mismo a partir de state.
// loading es true exactamente cuando state es "pending" o "refreshing"
const cargando = () => usuario.state === "pending" || usuario.state === "refreshing";
Como state es un tipo unión de cinco literales, encaja de maravilla con un Switch de ramas exclusivas: escribes un Match por fase relevante y TypeScript te ayuda a no olvidar ninguna. La diferencia con encadenar loading y error sueltos es que aquí modelas la interfaz como una función total del estado —a cada fase, una vista— en vez de como una secuencia de condiciones que podrían solaparse. Cuando el diseño de tu UI de carga distingue de verdad entre «primera vez» y «refrescando», leer state en lugar de loading es lo que te da esa resolución sin variables auxiliares.
En la práctica, state es además la propiedad que más miras al depurar. Registrar resource.state en un efecto te da una traza legible de cómo evolucionó una carga —unresolved, pending, ready, refreshing— mucho más clara que inferirla del baile de dos booleanos. Es el nombre que el propio resource le pone a su fase, y leerlo equivale a preguntarle directamente en qué punto de su vida se encuentra.
latest: el valor de ayer sin parpadeo
Aquí hay una diferencia sutil pero central entre llamar al accessor, usuario(), y leer usuario.latest. Bajo un límite de Suspense —o dentro de una transición—, leer usuario() mientras el resource está pending o refreshing suspende: dispara el fallback. latest, en cambio, nunca suspende: devuelve el último valor resuelto que hubo, o undefined si nunca lo hubo.
Esa propiedad la hace idiomática para búsquedas y paginación. latest te deja mantener a la vista el resultado anterior —atenuado, si quieres— mientras la nueva petición viaja, y sustituirlo solo cuando de verdad hay algo nuevo. El usuario nunca ve un hueco en blanco; ve una transición suave de un dato al siguiente.
function Resultados() {
const [q, setQ] = createSignal("");
const [resultados] = createResource(q, buscar);
// 'latest' mantiene la lista anterior visible mientras llega la nueva busqueda
return (
<ul classList={{ atenuado: resultados.loading }}>
<For each={resultados.latest ?? []}>
{(r) => <li>{r.titulo}</li>}
</For>
</ul>
);
}
La regla práctica: llama a usuario() cuando quieras que el JSX espere el dato nuevo (con Suspense o Show detrás), y lee usuario.latest cuando prefieras seguir mostrando el dato viejo hasta que el nuevo esté listo. La elección entre uno y otro es, en el fondo, una decisión de experiencia de usuario codificada en qué propiedad lees.
Hay una razón técnica detrás de esta diferencia que conviene nombrar. Leer el valor con usuario() participa en el mecanismo de suspensión: es la lectura la que le dice al Suspense que hay un dato pendiente y que debe mostrar su fallback. latest, por diseño, se abstiene de esa participación —lee sin señalar pendencia—, y por eso puede convivir con contenido ya renderizado sin colapsarlo. No es que una propiedad sea «mejor» que la otra: son dos contratos distintos con el sistema de carga, y elegir bien entre ellos es la diferencia entre una interfaz que parpadea a cada recarga y una que transiciona con suavidad. Este mismo eje —esperar frente a conservar— reaparecerá amplificado cuando estudiemos las transiciones, que generalizan el comportamiento de latest a árboles enteros.
Conviene, eso sí, no idealizar latest. Mostrar datos viejos mientras llegan los nuevos es deseable en una búsqueda, pero engañoso en un panel financiero donde un número obsoleto puede inducir a una decisión equivocada. La elección entre usuario() y usuario.latest es, por eso, tanto de producto como técnica —cuánta obsolescencia tolera esta pantalla en concreto— y merece pensarse caso por caso, en lugar de adoptar una por costumbre.
loading y error
Dos booleanos reactivos: hay peticion en vuelo, y el fetcher rechazo. Conectalos directo a Switch y Match sin estado intermedio.
state con cinco fases
Un valor que nombra la fase exacta. La clave es pending frente a refreshing: cargar sin datos, o cargar conservandolos.
latest sin parpadeo
Devuelve el ultimo valor resuelto sin suspender. El truco para busquedas y paginacion que no dejan la pantalla en blanco.
Cuando el fetcher rechaza, el resource no lanza la excepción hacia arriba de forma incontrolada: la atrapa y la deja disponible en usuario.error, que puedes leer reactivamente para renderizar un estado de fallo. Hay un matiz importante para más adelante: si lees el valor, usuario(), en un estado errored, ahí sí se re-lanza el error, y ese re-lanzamiento es lo que un ErrorBoundary captura. Es decir, tienes dos formas de tratar el fallo —leer error para manejarlo localmente, o dejar que usuario() lo propague a un límite de error superior—. Los ErrorBoundary y su interacción con los resources son el tema de un nivel próximo; por ahora, quédate con que el rechazo nunca se pierde: o lo lees en error, o sube por el valor.
La forma en que la mayoría de código trata la carga asíncrona es imperativa y frágil: declaras un booleano isLoading, otro para el error, quizá otro para «ya cargó una vez», y luego te pasas la vida sincronizándolos a mano en cada then, cada catch, cada reintento. Cada uno de esos booleanos es una oportunidad de que dos de ellos queden en un estado imposible —cargando y con error a la vez, o resuelto pero con el spinner colgado— porque nada garantiza que evolucionen de forma coherente. createResource invierte el modelo: no te da booleanos que sincronizar, te da una máquina de estados ya cableada que solo puedes leer. Los cinco valores de state no son cinco banderas independientes; son los nodos de un autómata cuyas transiciones —de unresolved a pending, de pending a ready o errored, de ready a refreshing— las gobierna el resource, no tú. Nunca verás un refreshing sin un valor previo, ni un error conviviendo con una carga limpia, porque esas combinaciones no son estados alcanzables del autómata. Esto tiene una consecuencia profunda en cómo escribes la interfaz: dejas de construir el estado de carga y pasas a observarlo. Tu JSX se vuelve una función pura del estado del resource —este state, este latest— en lugar de una coreografía de asignaciones. Y como la observación es reactiva y de grano fino, cada transición actualiza exactamente los nodos del DOM que dependían de la fase que cambió, ni uno más. La disciplina que este primitivo te enseña trasciende la carga de datos: es la de modelar cualquier proceso con estados como un valor que se lee, no como un puñado de variables que se manosean. Cuando esa forma de pensar se te vuelve reflejo, categorías enteras de bugs de estado inconsistente simplemente dejan de poder existir en tu código.
- Renderiza un resource con
Switch/Matchque distingaloadingyerror, y provoca un rechazo del fetcher para ver la rama de error. - Registra
resource.stateen un efecto y observa la secuencia real de fases al cargar, al recargar y al fallar. - Fuerza un
refreshing: carga, luego cambia la fuente, y confirma questatepasa porrefreshingy no porpending. - Sustituye
resource()porresource.latesten una lista y comprueba que el resultado anterior permanece visible mientras llega el nuevo. - Deriva tu propio
cargandoa partir destatey verifica que coincide siempre con la propiedadloadingdel resource.