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.
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.
- Contrastar la tupla de
createResourcecon el accessor desnudo decreateAsync. - Sustituir los estados
loadingyerrorpor las fronterasSuspenseyErrorBoundary. - Cambiar la fuente explícita del recurso por el rastreo automático de la derivación.
- Decidir con criterio cuándo
createResource—ocreateAsyncStore— 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.loadingahora la declara unSuspenseque envuelve a los consumidores. - Error que antes leías en
usuario.errorahora lo recoge unErrorBoundaryen la frontera. - Valor obsoleto que antes vivía en
usuario.latestsigue enlatest, la única propiedad que sobrevive en el accessor.
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
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:#11111bcreateResource
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.
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.
- Toma un
createResourcecon fuente y fetcher que pinta spinner conloadingy mensaje conerror, y reescríbelo comocreateAsyncconSuspenseyErrorBoundary. - Cuenta cuántos guardias defensivos (
?.,!,Show when) eliminaste al mover el estado a fronteras. - Sustituye una lista servida por
createResourceporcreateAsyncStoreconreconcileporidy confirma que revalidar no remonta las filas intactas. - Identifica un caso donde necesites
mutatepara una actualización optimista y argumenta por qué ahícreateResourcesigue siendo la elección. - Explica en una frase la diferencia entre “consultar
loading” y “declarar unSuspense”, y por qué la segunda concentra lo que la primera dispersa.