createResource: el fetcher y la fuente
Las dos firmas de createResource y la idea que las sostiene: un resource no es un envoltorio de fetch, es el primitivo que convierte una Promise en un signal cuyo valor llega más tarde. La firma mínima recibe un fetcher; la firma con fuente antepone un accessor reactivo cuyo valor se pasa al fetcher y cuyo cambio lo re-dispara. Una fuente falsy —false, null o undefined— le dice al resource que aún no toca pedir. Async como ciudadano de primera clase.
Cada primitivo que has construido hasta ahora resolvía su valor en el instante: un createSignal ya lo tiene, un createMemo lo calcula síncronamente. Lo asíncrono —una petición de red, la lectura de un archivo, cualquier Promise— rompía ese molde: su valor no está todavía, llega después. createResource es la respuesta de Solid a ese desajuste. No es un envoltorio de fetch ni un cliente HTTP: es el primitivo que convierte una promesa en un signal, de modo que «el dato que aún no ha llegado» pasa a ser un valor reactivo más que el grafo sabe leer, esperar y propagar. Este nivel abre presentando sus dos firmas y la tesis que lo sostiene: en Solid, lo asíncrono es un ciudadano de primera clase.
- Distinguir las dos firmas de
createResource: solo fetcher, y fuente más fetcher. - Interiorizar que un resource es un signal cuyo valor llega más tarde, no una promesa que esperas con
await. - Ver que el fetcher se re-ejecuta cuando la fuente cambia, sin que tú orquestes nada.
- Reconocer que una fuente falsy le indica al resource que todavía no toca pedir.
Un signal que resuelve una promesa
La firma mínima recibe un fetcher: cualquier función que devuelva una Promise. createResource la ejecuta de inmediato y te devuelve un accessor —un signal de solo lectura— cuyo valor es undefined mientras la promesa está en vuelo y pasa a ser el resultado en cuanto resuelve.
import { createResource } from "solid-js";
async function traerPerfil(): Promise<Perfil> {
const res = await fetch("/api/perfil");
return res.json();
}
const [perfil] = createResource(traerPerfil);
// perfil() es undefined hasta que la promesa resuelve; despues es el Perfil
Lo decisivo es que perfil no es la promesa: es un signal. No lo esperas con await, lo lees llamándolo. Y como es reactivo, leerlo dentro del JSX o de un efecto crea una suscripción: cuando el dato aterriza, todo lo que lo lee se actualiza sin una sola línea de orquestación.
Y no solo el JSX se beneficia: puedes leer el resource en cualquier ámbito reactivo. Un createEffect que dependa de perfil() correrá cuando el dato aterrice, lo que te da un gancho natural para reaccionar a la llegada —registrar una métrica, sincronizar con una API imperativa— sin cablear callbacks al fetcher. El resource es, a todos los efectos, un signal más del grafo; que su valor venga de la red es irrelevante para quien lo lee.
Un detalle de forma: createResource devuelve una tupla, y por eso escribimos const [perfil] = .... El segundo elemento, que aquí ignoramos, trae acciones para recargar y mutar el resource a mano —tema de una lección propia—. De momento nos basta el primer elemento, el accessor, que es donde viven el valor y todo su ciclo de vida.
Como todo primitivo con ciclo de vida, createResource debe crearse dentro de un ámbito reactivo —un componente o un createRoot—, porque su fetcher, su limpieza y su integración con Suspense se atan a un dueño. Llamarlo suelto al nivel de módulo lo dejaría sin ese anclaje; es la misma regla que gobierna a createSignal o createEffect, aplicada aquí a la carga de datos.
function Perfil() {
const [perfil] = createResource(traerPerfil);
return (
<Show when={perfil()} fallback={<p>Cargando...</p>}>
{(p) => <h1>{p().nombre}</h1>}
</Show>
);
}
Dos matices sobre el arranque. En la firma de un solo argumento, el fetcher se ejecuta de inmediato al crear el resource, sin esperar a nada: no hay fuente que aguardar. Y aunque aquí hemos usado Show con un fallback para el estado de carga, el compañero natural del resource es Suspense, que coordina varios resources bajo un mismo límite y muestra un único fallback mientras cualquiera de ellos carga. Ese primitivo protagoniza el próximo nivel; por ahora basta con saber que leer perfil() dentro de un Suspense hace que el límite espere el dato sin que tú toques ningún booleano de carga.
La fuente decide cuándo pedir
La segunda firma antepone una fuente reactiva al fetcher: createResource(fuente, fetcher). La fuente es un accessor —un signal, un memo, cualquier función que lea reactividad— y su valor se pasa al fetcher como primer argumento. La consecuencia es que el fetcher deja de correr una sola vez: corre cada vez que la fuente cambia.
const [id, setId] = createSignal(1);
const [usuario] = createResource(id, async (idActual) => {
const res = await fetch(`/api/usuario/${idActual}`);
return res.json();
});
setId(2); // la fuente cambio: el fetcher corre de nuevo, ahora con 2
Esto reproduce, sin createEffect ni listas de dependencias, el patrón que en otros frameworks exige coordinar a mano: «cuando el id cambie, vuelve a pedir». Aquí la relación es declarativa —el resource depende de la fuente— y el motor de grano fino se encarga del resto. La fuente no tiene por qué ser un signal desnudo: puede ser un createMemo, una función derivada o cualquier expresión reactiva, con tal de que sea una función que Solid pueda ejecutar y rastrear. Ese detalle, que parece técnico, es la puerta a combinar varias entradas en una sola fuente, algo que exploraremos en profundidad más adelante en el nivel.
Hay un contrato extra en la fuente que conviene grabar: si su valor es false, null o undefined, el fetcher no se llama. Solid lo interpreta como «aún no hay con qué pedir», y el resource se queda en su estado inicial sin disparar nada. Es el mecanismo idiomático para esperar a que exista un dato antes de lanzar la petición que depende de él.
const [id, setId] = createSignal<number | undefined>();
const [usuario] = createResource(id, traerUsuarioPorId);
// mientras id() sea undefined, no hay peticion: el resource espera
setId(7); // ahora la fuente es truthy: arranca la primera carga
Este contrato —fuente falsy, ninguna petición— es la forma idiomática de expresar «aún no hay con qué pedir» sin envolver el createResource en un if. Lo verás una y otra vez: no cargues el detalle hasta que se seleccione una fila, no pidas los datos protegidos hasta que exista una sesión. El resource permanece en su estado inicial, limpio y sin efectos, hasta que la fuente se vuelve significativa, y entonces arranca por sí solo.
flowchart LR S[fuente reactiva signal o memo] -->|valor truthy| F[fetcher recibe el valor] S -->|valor falsy| N[no se pide nada] F --> P[promesa en vuelo] P -->|resuelve| R[signal del resource actualizado] R --> V[el JSX que lo lee reacciona] style S fill:#89b4fa,color:#11111b style R fill:#a6e3a1,color:#11111b style N fill:#f9e2af,color:#11111b
El fetcher recibe el valor y su contexto
El fetcher recibe dos argumentos. El primero es el valor de la fuente. El segundo es un objeto de información con dos campos útiles: value, el último valor resuelto del resource, y refetching, que indica si esta ejecución nació de una recarga manual o de un cambio de la fuente.
const [pagina, setPagina] = createSignal(1);
const [lista] = createResource(pagina, async (p, info) => {
const previa = info.value ?? []; // el ultimo valor resuelto, o vacio la primera vez
const recarga = info.refetching; // true si vino de refetch; false si del cambio de fuente
const res = await fetch(`/api/items?pagina=${p}`);
const nuevos = await res.json();
// acumula para scroll infinito, o reemplaza segun el caso
return recarga ? nuevos : [...previa, ...nuevos];
});
Estos dos campos parecen menores pero habilitan patrones potentes. info.value te da acceso al valor anterior sin leerlo desde fuera, lo que permite acumular dentro del propio fetcher —una lista que crece página a página, fusionando lo nuevo con lo previo, en lugar de reemplazarlo—. info.refetching te deja distinguir la carga inicial de las recargas para, por ejemplo, mostrar un esqueleto solo la primera vez y un indicador más discreto después. La lección siguiente abre en canal el ciclo de vida completo que estos matices insinúan.
Merece subrayar una consecuencia de todo esto: createResource no reemplaza a tu cliente HTTP ni a tu capa de datos. El fetcher puede llamar a fetch, a un cliente de GraphQL, a un SDK o a una función de servidor —a lo que sea que devuelva una promesa—. El resource no opina sobre cómo traes el dato; solo se encarga de convertir esa promesa en reactividad. Esa modestia de alcance es justamente lo que lo hace universal: se pone encima de cualquier fuente asíncrona sin pedirle que cambie.
Y como cada resource es independiente, declarar varios en un mismo componente los lanza en paralelo, no en cascada: tres cargas sin dependencias entre sí viajan a la vez y resuelven cuando cada una puede. La cascada solo aparece si la construyes a propósito, encadenando la fuente de un resource al valor de otro —un patrón legítimo para dependencias reales que veremos más adelante en el nivel—.
El fetcher, además, no está obligado a ser async: cualquier función que devuelva algo thenable sirve, y si devuelve un valor síncrono el resource resuelve de inmediato. Pero su caso natural es la promesa, y es ahí donde createResource brilla: encapsula el ciclo completo de una operación diferida —pendiente, resuelta, fallida— en una única fuente reactiva que el resto de tu aplicación lee como si el tiempo no existiera.
Promesa a signal
El resource transforma una Promise en un accessor de solo lectura. No lo esperas: lo lees, y el grafo reacciona cuando el valor llega.
La fuente dispara
En la firma con fuente, el fetcher corre en cada cambio del accessor. Una fuente falsy pospone la peticion hasta que haya con que pedir.
El info del fetcher
El segundo argumento trae value, el valor anterior, y refetching, el origen de la ejecucion, para ramificar la carga con contexto.
El error más común al llegar de otros modelos es tratar el resource como una promesa: intentar await usuario o encadenarle un .then. No lo hagas. usuario es un accessor; su valor se lee llamándolo, usuario(), y su gracia es precisamente que no bloqueas ni encadenas: describes qué parte de la UI depende del dato y dejas que el grafo lo entregue cuando llegue. Si en algún punto necesitas la promesa cruda para coordinar en código imperativo, ese no es trabajo del resource, sino de la función que ya la produce.
El salto conceptual de este nivel entero cabe en una frase: createResource convierte el tiempo en un detalle de implementación. Piensa en lo que separa un valor síncrono de uno asíncrono. En términos de datos, nada: ambos son «el resultado de una operación». Lo único que difiere es cuándo está disponible —ahora o dentro de un rato—, y ese «cuándo» es exactamente lo que, en la mayoría de frameworks, contamina todo tu código: cadenas de promesas, banderas de carga cableadas a mano, efectos que sincronizan estado con peticiones, condiciones de carrera que hay que domar una a una. Solid hace una apuesta distinta y radical: si lo asíncrono es solo «un valor que aún no está», entonces el sistema reactivo —que ya sabe modelar valores que cambian en el tiempo— es el hogar natural para representarlo. Un signal es un valor que puede cambiar; un resource es un signal cuyo primer cambio es «pasar de no tener valor a tenerlo». Vistos así, no son dos primitivos, son el mismo con una semántica de arranque. Por eso lo lees igual, lo compones igual, lo derivas igual: para el grafo, que el dato viniera de la memoria o de un servidor a mil kilómetros es indistinguible una vez resuelto. Esta unificación es la que hace que, en Solid, la carga de datos no sea una capa aparte con su propia gramática, sino una consecuencia directa de la reactividad que ya dominas. Interioriza esto ahora, porque cada lección de este nivel —estados, refetch, cancelación, SSR— no es más que explotar la misma idea: lo asíncrono no es un caso especial que gestionar, es un valor que esperar, y el resource es el signal que te lo entrega.
- Escribe un resource de una sola firma sobre un
fetchy muéstralo conShowy un fallback; confirma queperfil()esundefinedantes de resolver. - Añade una fuente
idcon su firma de dos argumentos y comprueba que cambiarla con su setter re-dispara el fetcher con el nuevo valor. - Haz la fuente inicialmente
undefinedy verifica que no se lanza ninguna petición hasta que le das un valor truthy. - Lee
info.valueeinfo.refetchingdentro del fetcher y registra en consola de dónde vino cada ejecución. - Intenta a propósito hacer
awaitsobre el accessor del resource, observa que no obtienes el valor, y razona por qué leerlo en el JSX es lo correcto.