Caché y revalidación: invalidar por clave
Tras una mutación, los datos que tocó quedan obsoletos y hay que volver a pedirlos, pero solo los justos. Por qué una `action` revalida todas las queries por defecto, cómo acotar con la opción `revalidate` y `json`, la distinción capital entre `query.key` que invalida todas las instancias y `query.keyFor` que invalida una sola, cómo `revalidate` imperativo fuerza el refetch fuera de una acción y espera su promesa, y por qué el alcance de la clave es exactamente el alcance de la invalidación.
Cachear es fácil; saber cuándo la caché miente es el oficio. Una mutación deja obsoletos los datos que tocó: publicar una reseña invalida la lista de reseñas de ese producto, editar un precio invalida su ficha. Revalidar es declarar esa obsolescencia y volver a pedir —pero solo lo necesario—. SolidStart hace lo seguro por defecto: al completarse, una action revalida todas las queries. El oficio consiste en acotarlo con precisión quirúrgica, y la herramienta para hacerlo es la clave que ya conoces: key para invalidar todas las instancias de una query, keyFor para invalidar una sola. Nombrar bien tus queries, resulta, era ya diseñar tu estrategia de invalidación.
- Entender que una
actionrevalida todas las queries por defecto y por qué es lo seguro. - Acotar la revalidación a claves concretas con la opción
revalidateyjson. - Distinguir
query.key—todas las instancias— dequery.keyFor—una sola. - Forzar el refetch fuera de acciones con
revalidateimperativo y esperar su promesa.
Revalidar de más: el defecto seguro
Cuando una action termina con éxito, el router revalida todas las queries activas. La lógica es de seguridad: el framework no puede saber qué datos dependían de lo que mutaste, así que asume que cualquiera pudo quedar obsoleto y los recomputa todos. Es correcto siempre, pero pide de más: recarga queries que la mutación no rozó.
import { action } from "@solidjs/router";
// Sin acotar: al publicar una resena, TODAS las queries se revalidan
export const publicarResena = action(async (form: FormData) => {
"use server";
await db.resena.create({ data: leer(form) });
// ...sin mas, el router revalida getProducto, getProductos, getResenas...
}, "publicarResena");
Para una app pequeña esto basta y es lo más simple que funciona. En cuanto la superficie de datos crece, revalidar todo tras cada mutación se nota, y conviene decirle al router exactamente qué recomputar.
Invalidar solo lo necesario: key frente a keyFor
La forma declarativa de acotar es devolver el resultado con json y su opción revalidate, indicando qué claves recomputar. Solo esas queries se refrescan; el resto conserva su valor cacheado. Y aquí aparece la distinción capital entre las dos claves que expone cada query.
import { action, json } from "@solidjs/router";
import { getResenas } from "~/lib/productos";
export const publicarResena = action(async (form: FormData) => {
"use server";
const productoId = form.get("productoId") as string;
await db.resena.create({ data: leer(form) });
// recomputa solo las resenas de ESTE producto, nada mas
return json({ ok: true }, { revalidate: getResenas.keyFor(productoId) });
}, "publicarResena");
getResenas.key es el nombre base: revalidarlo recomputa todas las instancias de esa query, sin importar sus argumentos. getResenas.keyFor(id) es la clave de una instancia concreta: revalidarlo recomputa solo las reseñas de ese producto y deja intactas las demás cacheadas. Elegir entre una y otra es elegir el grano de tu invalidación.
revalidate: getResenas.keyFor(id); // solo ese producto
revalidate: getResenas.key; // todas las listas de resenas
revalidate: [getProducto.keyFor(id), getResenas.keyFor(id)]; // varias claves
La misma opción viaja en los otros resultados que una acción puede devolver: reload reejecuta la ruta actual acotando qué recomputar, y redirect navega a otra ruta arrastrando qué claves refrescar en el destino. Así la decisión de qué queda obsoleto acompaña a la acción sea cual sea su desenlace.
flowchart TD
M[action publicar resena en producto 7] --> D{que revalidar}
D -->|por defecto| A[todas las queries]
D -->|keyFor 7| B[solo resenas del producto 7]
D -->|key| C[todas las listas de resenas]
A --> T[mas trafico siempre correcto]
B --> P[minimo trafico preciso]
C --> G[grupo entero de esa query]
style A fill:#f9e2af,color:#11111b
style B fill:#a6e3a1,color:#11111b
style C fill:#fab387,color:#11111brevalidate imperativo
No toda revalidación nace de una acción. A veces quieres refrescar tras un evento externo —un websocket que avisa de un cambio, un botón de “actualizar”, un temporizador—. Para eso está la función revalidate, que dispara el refetch de una o varias claves desde cualquier sitio y devuelve una promesa que resuelve cuando el refetch termina.
import { revalidate } from "@solidjs/router";
await revalidate(getProducto.keyFor(id)); // refresca un producto
await revalidate(getResenas.key); // refresca toda una query
await revalidate(); // refresca absolutamente todo
Como devuelve una promesa, puedes esperar a que los datos frescos estén listos antes de, por ejemplo, cerrar un modal o mostrar un aviso de “hecho”. Es la misma maquinaria que las acciones usan por dentro, ahora a tu disposición para orquestar la coherencia desde fuera del flujo de formularios.
async function alRecibirWebsocket(id: string) {
await revalidate(getProducto.keyFor(id)); // trae la verdad fresca del servidor
mostrarAviso("actualizado"); // solo despues de que el dato ya esta al dia
}
Poder esperar la promesa cambia la calidad de tus interacciones: el aviso de “hecho”, el cierre del modal o la habilitación de un botón dejan de ser optimistas a ciegas y pasan a coincidir con el instante real en que el servidor ya devolvió datos frescos. La revalidación deja de ser un efecto secundario que disparas y olvidas para convertirse en un paso que puedes coordinar dentro de un flujo mayor.
El alcance de la clave es el alcance de la invalidación
Aquí converge todo. La clave con la que query indexa un dato no es solo cómo lo encuentras: es la unidad con la que lo invalidas. Si modelas “todas las reseñas” como una sola query sin argumentos, tu unidad de invalidación es la colección entera: publicar una te obliga a recomputar todas. Si modelas “las reseñas del producto N” como una query parametrizada, tu unidad baja a la instancia: publicar en el 7 revalida solo el 7. La granularidad de tu caché y la de tu invalidación son la misma decisión, tomada al elegir qué es una query y qué son sus argumentos.
Queda un matiz de ergonomía que redondea el patrón. Cuando revalidas una lista leída con createAsync, su valor es atómico: cada refetch reemplaza el objeto entero y un For sobre él remonta todas las filas aunque solo cambiara una. Leerla con createAsyncStore y reconcile resuelve esto: guarda el resultado en un store y cose la respuesta revalidada de grano fino sobre la anterior, preservando la identidad de lo que no cambió.
import { createAsyncStore } from "@solidjs/router";
// Cada revalidacion difea contra el valor previo por la clave id
const resenas = createAsyncStore(() => getResenas(id), {
reconcile: { key: "id" },
});
// el For solo remonta las filas que de verdad cambiaron
Así la invalidación por clave y la reconciliación por identidad se complementan: la primera decide qué colección se vuelve a pedir; la segunda, que esa colección aterrice sin remontar lo que ya estaba. Precisión al pedir, suavidad al pintar.
Acota por clave
key revalida todas las instancias de una query, keyFor solo una. Devuelve la clave con json y la opcion revalidate.
revalidate imperativo
Fuera de una accion, revalidate dispara el refetch de las claves que le pases y resuelve cuando termina.
Nombrar es diseñar
El grano al que nombras y parametrizas tus queries es el grano al que podras invalidarlas despues.
La tentación de optimizar prematuramente cada revalidate a la clave mínima suele salir cara: te acoplas a suposiciones sobre qué depende de qué, y el día que una mutación afecta a una query que olvidaste listar, sirves datos rancios —un bug silencioso y difícil de ver—. La disciplina prudente es la inversa: deja que las acciones revaliden de más al principio, que siempre es correcto, y acota solo las rutas calientes donde el tráfico extra se mida y moleste. Revalidar de más cuesta ancho de banda; revalidar de menos cuesta correctitud. Ante la duda, paga en bytes antes que en datos obsoletos.
El arco de la carga de datos se cierra en una idea que ahora se vuelve operativa: la clave con la que query indexa un dato es a la vez su dirección y su unidad de recomputación. Nombrar y parametrizar una query es, sin que lo pareciera, un acto de diseño de dependencias, porque ese nombre y sus argumentos definen el grano al que podrás declarar “esto quedó obsoleto”. Por eso key frente a keyFor no son dos funciones que eliges en el momento de invalidar, son dos niveles de un árbol que dibujaste al nombrar: el nombre base agrupa, la clave de instancia distingue, y tú decidiste ese árbol mucho antes, al trazar el mapa de queries. De ahí que revalidar bien no empiece en la acción sino en el diseño: una app cuyas queries están parametrizadas al grano de lo que muta puede invalidar con un bisturí; una cuyas queries son colecciones monolíticas solo puede invalidar con un mazo. Y observa cómo un solo concepto —el dato como entidad con clave y alcance— sostiene los cuatro patrones del nivel: la identidad con clave hizo posible la deduplicación; hizo posible la precarga, que deposita bajo una clave lo que otro leerá; hace posible la revalidación, que declara obsoleta una clave y recomputa exactamente su alcance; y hará posible el streaming, que decide por clave qué bloquea el HTML. Cuando lo veas así, dejarás de programar peticiones y empezarás a diseñar un grafo de datos con nombre cuya coherencia el framework mantiene por ti, invalidación a invalidación.
- Deja una
actioncon la revalidación por defecto y observa en red que tras la mutación se refrescan queries que no tenían nada que ver. - Acótala devolviendo
json(resultado, { revalidate: getResenas.keyFor(productoId) })y confirma que ahora solo se recarga esa lista. - Compara el efecto de
getResenas.keyygetResenas.keyFor(id)mutando dos productos distintos y viendo cuál recomputa de más. - Usa
revalidateimperativo desde un botón “actualizar” fuera de toda acción y espera su promesa antes de mostrar un aviso de “listo”. - Rediseña una query monolítica de colección en una parametrizada por id y argumenta cómo cambió el grano de invalidación disponible.