createAsync: una derivación asíncrona que se lee como un signal
createAsync, el primitivo asíncrono de Solid Router y anticipo del modelo de Solid 2.0: una promesa envuelta en una derivación reactiva que se lee llamándola, igual que cualquier signal. Cómo importarlo, cómo rastrea sus dependencias de forma automática como un memo, por qué debe leer sus fuentes antes del primer await, y cómo se integra con Suspense para que la lectura devuelva siempre un valor y nunca un estado de carga a mano.
Durante años el modelo asíncrono de Solid fue createResource: potente, pero con una forma —la tupla, la fuente explícita, los estados como propiedades— que delataba su parentesco con las herramientas de datos de React. createAsync, que llega desde Solid Router y prefigura el núcleo de Solid 2.0, borra esa distancia. Es una derivación asíncrona: escribes una función que devuelve una promesa y recibes a cambio un accessor que se lee llamándolo, exactamente como un signal. Cuando el dato aún no está, la lectura no te devuelve undefined ni un booleano de carga: suspende. Toda la incomodidad de coordinar lo asíncrono a mano se disuelve en un primitivo que se comporta como si el valor ya estuviera ahí.
- Importar
createAsyncde Solid Router y crear una derivación que se lee llamándola. - Entender que su función rastrea dependencias de forma automática, igual que un
createMemo. - Interiorizar por qué las fuentes reactivas deben leerse antes del primer
await. - Ver cómo la integración con
Suspensehace que la lectura devuelva siempre un valor.
De la promesa a la derivación
El punto de partida es una función que devuelve una promesa. createAsync la envuelve en una derivación reactiva y te entrega un accessor. No hay tupla, no hay opciones obligatorias, no hay fuente que declarar por separado: la dependencia se descubre leyendo, como en cualquier computación de Solid.
import { createAsync } from "@solidjs/router";
function Perfil(props: { id: number }) {
// se lee llamandolo, como un signal
const usuario = createAsync(() => getUsuario(props.id));
return (
<Suspense fallback={<Spinner />}>
<h1>{usuario().nombre}</h1>
</Suspense>
);
}
Lee usuario() dentro del JSX y observa lo que no está: ningún usuario.loading, ningún guardia if (!usuario()). Cuando props.id cambia, la función se vuelve a ejecutar, pide el usuario nuevo y el accessor entrega el resultado cuando llega. La lectura es idéntica a la de un signal ordinario; la asincronía queda debajo, invisible en el punto de uso.
El compañero natural de createAsync es query —el envoltorio de deduplicación y caché de Solid Router, antes llamado cache—. query memoiza la petición por su clave, la comparte entre lecturas y la hace revalidable; createAsync se limita a leerla dentro del grafo reactivo. Esta pareja es el patrón canónico del estado del servidor en Solid moderno.
import { query, createAsync } from "@solidjs/router";
// query dedupe y cachea por clave; se integra con preload y revalidacion
const getUsuario = query(async (id: number): Promise<Usuario> => {
const r = await fetch(`/api/usuarios/${id}`);
return r.json();
}, "usuario");
El rastreo es automático, como en un memo
Aquí está la idea que reordena todo lo demás: la función que pasas a createAsync rastrea sus dependencias sola, igual que la de un createMemo. Cualquier signal, prop reactiva o accessor que leas de forma síncrona dentro de ella queda suscrito, y el primitivo se re-ejecuta cuando esa fuente cambia. No declaras la dependencia; la contraes por el acto de leerla.
Eso tiene una consecuencia afilada que separa a quien entiende el modelo de quien lo sufre: el rastreo solo captura lo que se lee antes del primer await. En cuanto una función asíncrona suspende en un await, sale del contexto de rastreo síncrono; los signals leídos después ya no cuentan como dependencias. Es la misma trampa que acecha en los efectos asíncronos, y la disciplina es idéntica: lee tus fuentes arriba, antes de suspender.
// MAL: props.id se lee despues del await y NO queda rastreado
const datos = createAsync(async () => {
await precalentar();
return getUsuario(props.id); // esta lectura no crea dependencia
});
// BIEN: captura la dependencia de forma sincrona, antes del primer await
const datos = createAsync(async () => {
const id = props.id; // rastreado
await precalentar();
return getUsuario(id);
});
En la versión defectuosa, cambiar props.id no vuelve a disparar la derivación: la dependencia se perdió tras el await. En la correcta, id se leyó en terreno síncrono y la reactividad quedó cosida. Grábate la regla como reflejo, porque el compilador no te avisará.
Un await no pausa únicamente la ejecución: interrumpe el contexto reactivo. Toda fuente que tu derivación necesite para saber cuándo recalcularse tiene que leerse antes de esa frontera. Si un dato solo se conoce tras una petición previa, encadénalo en otra derivación (lo verás en la lección de composición) en lugar de leerlo tarde. Leer una dependencia después del primer await es la causa número uno de derivaciones que “no reaccionan” y de horas perdidas buscando un bug que no está en la red, sino en el orden de dos líneas.
Suspense integrado: el valor siempre está
La segunda mitad del diseño es que createAsync suspende por defecto. Cuando lees el accessor y el valor todavía no resolvió, la lectura lanza hacia el Suspense más cercano, que muestra su fallback mientras tanto. Por eso usuario() tiene tipo Usuario y no Usuario | undefined: dentro de la frontera, en el momento en que el contenido se pinta, el valor ya existe. La carga deja de ser un estado que compruebas y pasa a ser una frontera que declaras una vez.
<Suspense fallback={<p>Cargando perfil…</p>}>
{/* dentro de aqui, usuario() es un Usuario, nunca undefined */}
<h1>{usuario().nombre}</h1>
<p>{usuario().bio}</p>
</Suspense>
Los errores viajan por el mismo canal declarativo: una promesa rechazada se propaga al ErrorBoundary más cercano, igual que la carga viaja a Suspense. Y si prefieres arrancar con un valor sin suspender la primera vez —típico en SSR o para evitar un parpadeo—, la opción initialValue siembra el accessor con un dato inicial y desactiva la suspensión de arranque.
const total = createAsync(() => contarPendientes(), { initialValue: 0 });
// total() vale 0 desde el primer render; nunca suspende al inicio
Leer fuera del render: eventos y contexto reactivo
La lectura que suspende solo tiene sentido dentro de un contexto reactivo —el JSX, un memo, un efecto— donde una frontera Suspense puede atrapar lo pendiente. Si llamas al accessor desde un manejador de eventos o desde código que no rastrea, no hay nada que suspenda: la lectura devuelve el último valor resuelto, o undefined si aún no llegó ninguno. Por eso, para usar un valor asíncrono dentro de un evento, léelo por latest y defiende el caso de que todavía no exista.
const usuario = createAsync(() => getUsuario(props.id));
function alGuardar() {
// en un evento no hay Suspense que atrape lo pendiente
const u = usuario.latest; // valor actual o previo, sin suspender
if (!u) return; // defiende el caso de que aun no haya llegado
guardarPreferencias(u.id);
}
La regla operativa cabe en una línea: dentro del render, lee llamando y deja que Suspense haga su trabajo; fuera del render, lee latest y defiende el undefined a mano, porque ahí no hay frontera que te cubra. Confundir los dos contextos es la causa más común de un undefined inesperado que solo aparece al pulsar un botón.
flowchart LR S[signal o prop reactiva] -->|lectura sincrona| F[funcion async de createAsync] F -->|devuelve promesa| P[promesa pendiente] P -->|resuelve| A[accessor con valor] A -->|se lee llamandolo| J[JSX] A -.aun pendiente.-> B[Suspense muestra fallback] A -.rechazada.-> E[ErrorBoundary captura] style A fill:#a6e3a1,color:#11111b style B fill:#f9e2af,color:#11111b style E fill:#f38ba8,color:#11111b
Se lee llamando
Un accessor como cualquier signal. usuario() devuelve el valor; no hay tupla ni propiedades de estado que consultar en el punto de uso.
Rastreo automatico
Las dependencias se descubren leyendo, como en un memo. Se re-ejecuta sola cuando cambia una fuente leida antes del primer await.
Suspense por defecto
Leer un valor pendiente suspende hacia la frontera mas cercana. La carga es una frontera declarada, no un booleano comprobado.
La grandeza de este primitivo no está en lo que añade sino en lo que quita. createResource te obligaba a sostener en la cabeza tres verdades a la vez —el dato, si estaba cargando, si había fallado— y a hilarlas por toda la interfaz con guardias defensivos. createAsync colapsa esas tres en una sola lectura que se comporta como si el valor siempre estuviera presente, y traslada la carga y el error a fronteras declaradas una vez, arriba, con Suspense y ErrorBoundary. El truco conceptual es que una promesa se disfraza de signal: por fuera se lee llamándola, participa del grafo, rastrea dependencias como un memo y se recalcula cuando una fuente cambia; por dentro coordina con la infraestructura de suspensión para que el instante en que el dato no existe nunca aflore al código que lo consume. Esa ficción —tratar lo asíncrono como si fuera síncrono y dejar que el sistema gestione la espera— no es azúcar sintáctico: es el modelo que Solid 2.0 lleva al corazón del sistema reactivo, donde cualquier derivación podrá ser asíncrona y lo pendiente fluirá por el grafo como un estado de primera clase. Aprender createAsync hoy no es aprender una función de una librería de rutas; es adoptar por adelantado la forma en que se escribirán los datos en Solid durante la próxima década. Y la única disciplina que exige a cambio es respetar la frontera del await: leer las dependencias antes de suspender, porque el rastreo síncrono termina donde empieza la promesa. Quien interioriza esa regla escribe datos asíncronos con la misma naturalidad con que escribe un memo, y deja de coordinar estados para empezar a declarar derivaciones.
- Crea un
getUsuarioconqueryy léelo desde uncreateAsync; pinta el nombre dentro de unSuspensey confirma que nunca escribesusuario()?.nombrecon interrogante. - Cambia la prop
iddesde un botón y verifica que la derivación se vuelve a disparar y elfallbackreaparece durante la recarga. - Introduce a propósito el bug del
await: leeprops.iddespués de unawaity comprueba que cambiar el id ya no recarga; arréglalo subiendo la lectura. - Añade
initialValuea una segunda derivación y observa que ya no suspende en el primer render. - Envuelve la lectura en un
ErrorBoundary, haz que la promesa rechace y confirma que el error viaja a la frontera sin un solotry/catchen el componente.