query y createAsync: el patrón canónico de carga
La forma canónica de cargar datos en SolidStart es un par: `query` envuelve el fetcher y le da una identidad memoizada por clave, y `createAsync` lo lee de forma reactiva integrándose con `Suspense`. Cómo colocar las queries en un módulo de datos, por qué el fetcher suele cruzar a servidor con `use server`, cómo la clave se compone del nombre y los argumentos serializados, qué exponen `keyFor` y `key`, y por qué el alcance de la caché es una petición en el servidor y una pestaña en el cliente.
El fetcher desnudo y sus tres grietas
Un componente que pide su propio dato con fetch parece inocente, pero arrastra tres problemas que no se ven en la demo y sí en producción. No hay deduplicación: si tres componentes muestran el mismo producto, salen tres peticiones idénticas. No hay caché: al navegar fuera y volver, se repite el trabajo entero. Y encaja mal con la suspensión: coordinar el estado de carga a mano es tedioso y frágil.
// Ingenuo: cada componente reinventa la peticion, sin coordinacion ni cacheasync function Ficha(props: { id: string }) { const res = await fetch(`/api/productos/${props.id}`); const producto = await res.json(); return <h1>{producto.nombre}</h1>;}El diagnóstico de fondo no es de rendimiento: es que “el producto 7” debería ser una sola cosa que la app referencia, no una petición que cada consumidor vuelve a inventar. La cura es dar a cada dato una clave estable y una única fuente memoizada.
query: un fetcher con identidad
query recibe tu función asíncrona y un nombre único, y devuelve otra función con la misma firma pero memoizada por clave. Se coloca en un módulo de datos —lejos de los componentes— para que toda la app importe la misma instancia. El fetcher suele cruzar a servidor con la directiva use server, de modo que la consulta a la base de datos nunca viaja al navegador.
// ~/lib/productos.tsimport { query } from "@solidjs/router";export const getProducto = query(async (id: string) => { "use server"; const producto = await db.producto.findUnique({ where: { id } }); if (!producto) throw new Error("producto no encontrado"); return producto;}, "producto");export const getProductos = query(async () => { "use server"; return db.producto.findMany({ orderBy: { nombre: "asc" } });}, "productos");El segundo argumento —"producto", "productos"— es el nombre base de la query y debe ser único en toda la app: es la mitad de su clave. Llamar a getProducto("7") desde varios sitios a la vez no dispara varias peticiones: la primera arranca la red y las demás reciben la misma promesa en vuelo; cuando resuelve, el valor queda cacheado bajo esa clave.
createAsync: el puente reactivo
query por sí sola no es reactiva. El puente al grafo es createAsync: le pasas una función que llama a la query y te devuelve un accessor que se suspende mientras carga e integra con Suspense sin una línea extra.
import { createAsync } from "@solidjs/router";import { Suspense } from "solid-js";import { getProducto } from "~/lib/productos";function Ficha(props: { id: string }) { const producto = createAsync(() => getProducto(props.id)); return ( <Suspense fallback={<Esqueleto />}> <h1>{producto()?.nombre}</h1> <p>{producto()?.precio}</p> </Suspense> );}Como createAsync rastrea props.id, cuando el id cambia se relee la query con la clave nueva; si ese valor ya estaba cacheado, aparece sin parpadeo. El accessor vale undefined hasta que la primera carga resuelve —de ahí el ?. al leerlo—; si prefieres eliminar ese hueco, pasa un initialValue en las opciones, y bajo transiciones el accessor conserva el valor anterior mientras el nuevo carga en vez de volver a undefined.
Cuando una pantalla necesita varios datos, la regla de oro es no encadenarlos: declara cada createAsync por separado para que sus queries salgan en paralelo, en lugar de esperar una para lanzar la siguiente y encadenar un waterfall. Solo cuando un dato depende de verdad del anterior —su argumento sale de la primera respuesta— se justifica leerlo dentro del segundo accessor. Y como el accessor es un signal más, puedes derivarlo con un createMemo, pasarlo por props o leerlo en otro efecto: toda la maquinaria de grano fino se le aplica igual.
// Paralelo: ambas queries salen a la vez, sin cascadaconst producto = createAsync(() => getProducto(props.id));const resenas = createAsync(() => getResenas(props.id));// Dependiente: solo si el segundo necesita el resultado del primeroconst relacionados = createAsync(async () => { const p = await getProducto(props.id); return getRelacionados(p.categoria);});La clave y el alcance de la caché
La clave que memoiza cada entrada se compone del nombre que diste y de los argumentos serializados. Dos utilidades exponen esa clave para operar sobre la caché más adelante: getProducto.keyFor("7") devuelve la clave de una instancia concreta, y getProducto.key devuelve el nombre base que agrupa a todas sus instancias. Las usarás sin falta al revalidar.
getProducto.keyFor("7"); // clave de un producto concretogetProducto.key; // nombre base: agrupa todos los productosEl detalle que separa a Solid de una caché casera es el alcance. En el cliente, la caché vive lo que la pestaña, así que navegar y volver reutiliza el dato. En el servidor —bajo SSR— la caché está atada a la petición actual: nace al entrar la solicitud y muere con su respuesta, lo que impide que el dato de un usuario se filtre en la respuesta de otro. Ese doble alcance es también lo que permite la serialización transparente: cuando el servidor resuelve una query durante el SSR, su valor viaja en el HTML y rehidrata la caché del cliente, de modo que el primer render en el navegador no repite la petición. La query es, en ese sentido, un canal que cruza la frontera red —se llena en el servidor y se lee en el cliente sin una segunda ida al backend—, y esa continuidad entre entornos es la base sobre la que se apoyan la precarga y el streaming que verás a continuación.