Saltar al contenido

Deduplicación y caché con query

El patrón fundacional del data fetching en SolidStart: envolver un fetcher en `query` para que el framework lo memoice por clave. Cómo `query` combina el dedup de llamadas concurrentes con la caché del último valor, cómo `createAsync` lee esa query de forma reactiva e integra con Suspense, cómo se deriva la clave a partir del nombre y los argumentos serializados, y por qué el alcance de la caché es una sola petición en el servidor y la sesión de la pestaña en el cliente.

El problema: la misma petición, muchas veces

Sin una capa de caché, cada componente que necesita un dato lanza su propio fetch. Si tres componentes muestran el mismo usuario, salen tres peticiones idénticas al mismo endpoint, en el mismo instante, por el mismo valor. Y peor: al navegar fuera y volver, el trabajo se repite entero, aunque el dato no haya cambiado.

TSX
// Ingenuo: cada Perfil dispara su propia peticion, sin coordinacionasync function Perfil(props: { id: string }) {  const res = await fetch(`/api/usuarios/${props.id}`);  const usuario = await res.json();  return <h1>{usuario.nombre}</h1>;}// Tres <Perfil id="7" /> = tres peticiones identicas

El problema no es de rendimiento sino de identidad de los datos: “el usuario 7” debería ser una sola cosa compartida por toda la app, no una petición que cada consumidor reinventa. La solución es dar a cada dato una clave estable y una única fuente memoizada.

query: memoizar un fetcher por clave

query toma tu función asíncrona y un nombre único, y devuelve una función con las mismas firmas pero memoizada. Todas las llamadas con los mismos argumentos comparten una sola ejecución en vuelo y reutilizan el último valor resuelto.

TSX
// data/usuarios.tsimport { query } from "@solidjs/router";export const getUsuario = query(async (id: string) => {  const res = await fetch(`/api/usuarios/${id}`);  if (!res.ok) throw new Error("usuario no encontrado");  return (await res.json()) as Usuario;}, "usuario");

El segundo argumento, "usuario", es el nombre base de la query y debe ser único en toda la app: es la mitad de su clave. La otra mitad son los argumentos. Llamar a getUsuario("7") desde tres sitios a la vez no dispara tres peticiones: la primera arranca la red y las otras dos reciben la misma promesa en vuelo. Cuando resuelve, el valor queda cacheado bajo esa clave, y una cuarta llamada posterior lo devuelve sin tocar la red.

createAsync lee la query de forma reactiva

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 de fontanería.

TSX
import { createAsync } from "@solidjs/router";import { Suspense } from "solid-js";function Perfil(props: { id: string }) {  const usuario = createAsync(() => getUsuario(props.id));  return (    <Suspense fallback={<Cargando />}>      <h1>{usuario()?.nombre}</h1>    </Suspense>  );}

Como createAsync rastrea props.id, cuando el id cambia se vuelve a leer la query con la clave nueva; si ese valor ya está cacheado, aparece sin parpadeo. Y si diez componentes distintos hacen createAsync(() => getUsuario("7")), la deduplicación de query garantiza una sola petición para todos.

El accessor que devuelve createAsync vale undefined hasta que la primera carga resuelve —de ahí el ?. al leerlo fuera de un Suspense—. Si prefieres un valor de arranque para evitar ese hueco, pásalo en las opciones; y bajo transiciones el accessor conserva el valor anterior mientras el nuevo carga, en vez de volver a undefined, lo que evita parpadeos al recargar datos que ya tenías.

TSX
// initialValue elimina el undefined inicial; util para listasconst usuario = createAsync(() => getUsuario(props.id), {  initialValue: undefined,  deferStream: true, // en SSR, espera a este dato antes de emitir HTML});

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: getUsuario.keyFor("7") devuelve la clave de esa instancia concreta, y getUsuario.key devuelve el nombre base que agrupa a todas sus instancias. Las necesitarás en cuanto llegues a la revalidación.

TSX
getUsuario.keyFor("7"); // clave de un usuario concretogetUsuario.key;         // nombre base: agrupa todos los usuarios

Ahora la parte que separa a Solid de una caché casera: el alcance. En el cliente, la caché vive lo que la pestaña, así que navegar y volver reutiliza el dato en vez de repedirlo. En el servidor —bajo SSR— la caché está atada a la petición actual: se crea al entrar la solicitud y muere con su respuesta. Ese aislamiento por petición es exactamente lo que evita que el dato de un usuario se filtre en la respuesta de otro, sin que tú tengas que pensarlo.

Ese doble alcance es también lo que hace posible la serialización transparente entre servidor y cliente. Cuando el servidor resuelve una query durante el SSR, su valor viaja en el HTML y se rehidrata en la caché del cliente, de modo que el primer render en el navegador no repite la petición que el servidor ya hizo. 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. Para escribir a esa caché sin pasar por la red —tras una mutación local, por ejemplo— la propia query expone utilidades, pero eso pertenece ya al patrón de revalidación.