wandres.dev
DERIVACIONES ASYNC · createAsync y el futuro

createAsync frente a createResource: sin tupla, se lee llamando, Suspense por defecto

El mapa exacto de diferencias entre createResource y createAsync: la tupla con mutate y refetch frente al accessor desnudo, los estados loading y error como propiedades frente a las fronteras Suspense y ErrorBoundary, la fuente explícita frente al rastreo automático como en un memo, y la mutación optimista imperativa frente al modelo de revalidación por query. Cuándo cada uno sigue siendo la herramienta correcta y por qué createAsyncStore hereda el patrón de reconcile del nivel de estado.

⏱ 17 min

createAsync no es un createResource con otro nombre: es una relectura del mismo problema desde una filosofía distinta. Donde el recurso te entrega un panel de control —el dato, la carga, el error, botones de mutate y refetch— la derivación async te entrega un valor que se lee y ya. Donde el recurso te pide declarar la fuente por separado, la derivación la rastrea sola. Y donde el recurso expone la carga y el error como propiedades que consultas, la derivación las emite hacia fronteras que declaras una vez. Entender estas diferencias no es memorizar dos APIs: es ver qué modelo mental encarna cada una y cuándo cada uno es el correcto.

🎯 Al terminar esta lección sabrás
  • Contrastar la tupla de createResource con el accessor desnudo de createAsync.
  • Sustituir los estados loading y error por las fronteras Suspense y ErrorBoundary.
  • Cambiar la fuente explícita del recurso por el rastreo automático de la derivación.
  • Decidir con criterio cuándo createResource —o createAsyncStore— sigue ganando.

La tupla que desaparece

createResource devuelve una tupla: un accessor de datos enriquecido con loading, error y latest, más un objeto con mutate y refetch. Es un objeto-panel: todo lo que puedes saber y hacer con la petición cuelga de él.

import { createResource } from "solid-js";

const [usuario, { mutate, refetch }] = createResource(
  () => props.id,          // fuente explicita
  (id) => getUsuario(id)   // fetcher que recibe la fuente
);

usuario();          // el valor, o undefined mientras carga
usuario.loading;    // boolean de carga
usuario.error;      // el error si la promesa rechazo
usuario.latest;     // ultimo valor durante una recarga

createAsync colapsa todo eso en un accessor con una sola propiedad extra, latest. No hay loading, no hay error, no hay mutate ni refetch colgando del valor: la carga se declara con Suspense, el error con ErrorBoundary, y la revalidación la gobierna el query que lees.

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

const usuario = createAsync(() => getUsuario(props.id));

usuario();          // el valor; SUSPENDE si aun no esta
usuario.latest;     // ultimo valor sin suspender
// loading  -> lo maneja <Suspense>
// error    -> lo maneja <ErrorBoundary>
// refetch  -> revalidate(getUsuario.key) del router

La reducción no es cosmética. Con la tupla, cada componente que consume el recurso carga con la responsabilidad de comprobar loading y error y de pintar undefined con guardias. Con el accessor desnudo, esa responsabilidad sube a una frontera y desaparece del punto de uso: lees un valor que, dentro del Suspense, siempre está.

De propiedades de estado a fronteras declarativas

El cambio más profundo es dónde vive el estado de la petición. En createResource vive en el valor: preguntas usuario.loading allí donde lo necesites, y el spinner es una rama del render de ese componente. En createAsync vive en el árbol: la carga es una frontera Suspense que envuelve a uno o muchos consumidores, y el error una frontera ErrorBoundary. Un mismo Suspense coordina la carga de varias derivaciones a la vez; un mismo ErrorBoundary recoge el fallo de todas.

// createResource: el estado se comprueba en cada punto de uso
<Show when={!usuario.loading} fallback={<Spinner />}>
  <Show when={!usuario.error} fallback={<Error err={usuario.error} />}>
    <h1>{usuario()!.nombre}</h1>
  </Show>
</Show>

// createAsync: el estado son fronteras que envuelven, no ramas que comprueban
<ErrorBoundary fallback={(err) => <Error err={err} />}>
  <Suspense fallback={<Spinner />}>
    <h1>{usuario().nombre}</h1>
  </Suspense>
</ErrorBoundary>

Compara las dos columnas: la primera dispersa la lógica de estado por cada lectura; la segunda la concentra en dos fronteras y deja el cuerpo limpio. Esta es la misma inversión que Solid hizo con la reactividad —del control imperativo a la declaración— aplicada ahora a lo asíncrono.

Los tres canales de estado quedan así repartidos:

  • Carga que antes leías en usuario.loading ahora la declara un Suspense que envuelve a los consumidores.
  • Error que antes leías en usuario.error ahora lo recoge un ErrorBoundary en la frontera.
  • Valor obsoleto que antes vivía en usuario.latest sigue en latest, la única propiedad que sobrevive en el accessor.
ℹ️
El rastreo automatico frente a la fuente explicita

createResource separa la fuente del fetcher: el primer argumento es un accessor cuyo cambio dispara la recarga, y el segundo recibe su valor. Ese diseño es explícito pero verboso, y obliga a nombrar la dependencia. createAsync no tiene fuente separada: su función rastrea las dependencias leyéndolas, como un createMemo. Ganas concisión y pierdes un punto de control; a cambio, la disciplina del await (leer las fuentes antes de suspender) pasa a ser tu responsabilidad. Es el mismo trueque que Solid ofrece entre on explícito y el rastreo implícito, trasladado al mundo async.

Cuándo cada uno sigue siendo el correcto

createAsync es el nuevo idioma por defecto para leer estado del servidor, pero createResource no se jubila. Elige el recurso cuando necesites sus controles imperativos: mutate para una actualización optimista inmediata antes de que la red confirme, o refetch para forzar una recarga puntual sin pasar por el sistema de query. El recurso te da los mandos en la mano; la derivación te da un modelo declarativo donde la revalidación la orquesta el router.

Y cuando quieras lo mejor de dos mundos —lectura como signal y grano fino sobre colecciones—, Solid Router ofrece createAsyncStore, que envuelve el mismo modelo pero reconcilia cada respuesta contra un store interno. Es la industrialización del patrón que ya montaste a mano en el nivel de estado: recurso para el ciclo, store para el grano fino, reconcile para coser cada instantánea preservando identidad. Con createAsyncStore una lista revalidada no remonta las filas que no cambiaron.

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

// se lee como createAsync, pero por dentro reconcilia: grano fino en colecciones
const todos = createAsyncStore(() => getTodos(), { reconcile: { key: "id" } });
// <For each={todos()}> no remonta filas intactas al revalidar

Y donde createResource te ofrecía refetch, createAsync delega la recarga en el sistema de query: llamas a revalidate con la clave de la consulta —o dejas que una action la invalide sola tras una mutación— y todas las derivaciones que la leen se recalculan a la vez. La imperatividad de recargar no desaparece, se centraliza en la caché en lugar de colgar de cada valor, y eso es justo lo que permite que una escritura invalide de golpe cuantas vistas dependan del dato.

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

// fuerza la recarga de una query; toda createAsync que la lea se actualiza
await revalidate(getUsuario.keyFor(props.id));
// o revalidate(getUsuario.key) para invalidar todas sus variantes
📝
mutate vive en el recurso, la revalidación vive en la caché

Es el reparto que resume la diferencia entre los dos modelos. El mutate de createResource escribe el valor al instante y en local, sin tocar la red: un bisturí para el optimismo puntual. revalidate sobre un query no escribe nada; marca la caché como obsoleta y deja que la próxima lectura vuelva a pedir. Uno corrige un dato concreto, el otro declara una verdad —esto ya no es de fiar— y confía en que el sistema reaccione. Elegir entre ambos es elegir entre retocar un valor y caducar una fuente.

flowchart TD
A[necesito leer estado del servidor] --> B{necesito mutate o refetch imperativos}
B -->|si| R[createResource con su tupla de control]
B -->|no| C{la respuesta es una coleccion con identidad}
C -->|si| S[createAsyncStore reconcilia grano fino]
C -->|no| D[createAsync accessor desnudo]
style D fill:#a6e3a1,color:#11111b
style S fill:#89b4fa,color:#11111b
style R fill:#f9e2af,color:#11111b
🎛️

createResource

Tupla con loading, error, mutate y refetch. Fuente explicita. Ideal cuando quieres los mandos imperativos en la mano.

📶

createAsync

Accessor desnudo con latest. Rastreo automatico. Estados en fronteras. El idioma por defecto del estado del servidor.

🧬

createAsyncStore

createAsync mas reconcile por dentro. Grano fino sobre colecciones sin remontar filas intactas al revalidar.

No es una API nueva: es el estado del servidor dejando de fingir que es estado del cliente

La tentación al ver createAsync es catalogarlo como un createResource más ergonómico y seguir pensando igual. Sería perder la lección entera. Lo que cambia entre uno y otro no es la sintaxis sino la ontología de lo asíncrono. createResource nació cuando lo async se modelaba como un objeto con estado —un dato que puede estar cargando o roto— y por eso te entrega un panel: consultas loading, consultas error, actúas con refetch. Ese modelo trata la petición como una cosa que posees y administras. createAsync parte de la idea opuesta: el estado del servidor no es tuyo, es el reflejo local de una verdad remota que solo conoces por instantáneas, y por tanto no tiene sentido administrarlo con mandos sino declararlo como una derivación que se recalcula cuando sus fuentes cambian. La carga y el error dejan de ser propiedades que interrogas para volverse condiciones del árbol que declaras en fronteras —Suspense cuando el reflejo aún no llegó, ErrorBoundary cuando llegó roto—. El accessor desnudo no es minimalismo estético: es la afirmación de que en el punto de uso solo debería existir el valor, porque el resto son estados transitorios que pertenecen a la estructura, no al dato. Por eso createAsync compone, se rastrea y suspende como cualquier derivación, mientras createResource sigue siendo el recurso adecuado justo cuando necesitas romper esa pureza y tomar los mandos —una mutación optimista, una recarga forzada—. Saber cuál eliges deja de ser una cuestión de preferencia y pasa a ser una declaración sobre cómo entiendes el dato que tienes entre manos: una posesión que administras, o un reflejo que declaras.

⚔️ Traduce de un modelo al otro
  1. Toma un createResource con fuente y fetcher que pinta spinner con loading y mensaje con error, y reescríbelo como createAsync con Suspense y ErrorBoundary.
  2. Cuenta cuántos guardias defensivos (?., !, Show when) eliminaste al mover el estado a fronteras.
  3. Sustituye una lista servida por createResource por createAsyncStore con reconcile por id y confirma que revalidar no remonta las filas intactas.
  4. Identifica un caso donde necesites mutate para una actualización optimista y argumenta por qué ahí createResource sigue siendo la elección.
  5. Explica en una frase la diferencia entre “consultar loading” y “declarar un Suspense”, y por qué la segunda concentra lo que la primera dispersa.