Saltar al contenido

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.

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.

TSX
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.

TSX
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.

TSX
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.

TSX
const [id, setId] = createSignal<number | undefined>();const [usuario] = createResource(id, traerUsuarioPorId);// mientras id() sea undefined, no hay peticion: el resource esperasetId(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.

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.

TSX
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.