Revalidación: invalidar la caché tras mutar
Tras una mutación, volver a pedir solo lo necesario. Por qué una `action` revalida todas las queries por defecto, cómo acotar la revalidación a una clave concreta con `json` y la opción `revalidate`, cómo `getX.key` y `getX.keyFor` distinguen entre invalidar todas las instancias de una query o una sola, cómo `revalidate` imperativo fuerza el refetch fuera de una acción, y cómo `createAsyncStore` con `reconcile` hace que la revalidación actualice de grano fino en vez de remontar.
Una mutación deja obsoletos los datos que tocó: crear una tarea invalida la lista, editar un usuario invalida su perfil. 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, garantizando coherencia a costa de tráfico. El oficio consiste en acotarlo: con la opción revalidate y json marcas qué claves recomputar, y getX.key frente a getX.keyFor distingue entre invalidar todas las instancias de una query o una sola. Con revalidate imperativo fuerzas un refetch fuera de una acción; y con createAsyncStore más reconcile, la respuesta revalidada se cose de grano fino en vez de remontar la lista entera.
- 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
getX.key—todas las instancias— degetX.keyFor—una sola—. - Forzar refetch fuera de acciones con
revalidateimperativo y difear concreateAsyncStore.
La revalidación automática de las acciones
El comportamiento por defecto es deliberadamente conservador: 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ó.
// Sin acotar: al crear un todo, TODAS las queries se revalidan
export const addTodo = action(async (texto: string) => {
await fetch("/api/todos", { method: "POST", body: JSON.stringify({ texto }) });
// ...sin mas, el router revalida getTodos, getUsuario, getFacturas...
}, "addTodo");
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
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.
import { action, json } from "@solidjs/router";
import { getTodos } from "~/data/todos";
export const addTodo = action(async (texto: string) => {
const nuevo = await crearEnServidor(texto);
return json(nuevo, { revalidate: getTodos.key }); // solo la lista de todos
}, "addTodo");
Aquí aparece la distinción crucial entre las dos claves que ya conociste. getTodos.key es el nombre base: revalidarlo recomputa todas las instancias de esa query, sin importar sus argumentos. getUsuario.keyFor("7") es la clave de una instancia concreta: revalidarlo recomputa solo el usuario 7 y deja intactos los demás usuarios cacheados.
revalidate: getUsuario.keyFor(id); // solo ese usuario
revalidate: getUsuario.key; // todos los usuarios
revalidate: [getTodos.key, getUsuario.keyFor(id)]; // varias claves
La misma opción revalidate viaja en los otros dos resultados que una acción puede devolver. reload reejecuta la ruta actual acotando qué recomputar, y redirect navega a otra ruta arrastrando consigo 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 —quedarse, recargar o redirigir—.
import { action, redirect, reload } from "@solidjs/router";
export const borrarTodo = action(async (id: string) => {
await borrarEnServidor(id);
return reload({ revalidate: getTodos.key }); // recarga la ruta, solo la lista
}, "borrarTodo");
export const crearFactura = action(async (datos: DatosFactura) => {
const nueva = await crearEnServidor(datos);
throw redirect(`/facturas/${nueva.id}`, { revalidate: getFacturas.key });
}, "crearFactura");
flowchart TD
M[action editar usuario 7] --> D{que revalidar}
D -->|por defecto| A[todas las queries]
D -->|keyFor 7| B[solo usuario 7]
D -->|key| C[todos los usuarios]
A --> T[mas trafico, siempre correcto]
B --> P[minimo trafico, preciso]
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.
import { revalidate } from "@solidjs/router";
await revalidate(getUsuario.keyFor(id)); // refresca un usuario
await revalidate(getTodos.key); // refresca toda una query
await revalidate(); // refresca absolutamente todo
revalidate devuelve una promesa que resuelve cuando el refetch termina, así que puedes esperar a que los datos frescos estén listos antes de, por ejemplo, cerrar un modal. Es la misma maquinaria que usan las acciones por dentro, ahora a tu disposición.
Revalidar sin remontar: createAsyncStore
Revalidar trae la colección entera de nuevo, y si la lees con createAsync, su valor es atómico: cada refetch reemplaza el objeto completo y un For sobre él remonta todas las filas, aunque solo cambiara una. createAsyncStore resuelve esto: guarda el resultado en un store y aplica reconcile en cada revalidación, cosiendo la respuesta nueva sobre la vieja y 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 todos = createAsyncStore(() => getTodos(), {
reconcile: { key: "id" },
});
// <For each={todos()}> solo remonta las filas que de verdad cambiaron
Con esto, la lección anterior y esta encajan: la mutación optimista pinta el futuro al instante, la revalidación trae la verdad del servidor, y reconcile hace que esa verdad aterrice de grano fino —la fila fantasma cede su sitio a la real sin que el resto de la lista parpadee—.
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.
Difea con store
createAsyncStore aplica reconcile en cada revalidacion, actualizando de grano fino en vez de remontar la lista.
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, prefiere pagar en bytes antes que en datos obsoletos.
Todo este nivel converge en una idea que ahora se vuelve operativa: la clave con la que query indexa un dato no es solo cómo lo encuentras, es la unidad con la que lo invalidas. Nombrar 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”. Si modelas “todos los usuarios” como una sola query sin argumentos, tu unidad de invalidación es toda la colección: editar uno te obliga a recomputar todos. Si modelas “el usuario N” como una query parametrizada, tu unidad baja a la instancia: editar el 7 revalida solo el 7. La granularidad de tu caché y la granularidad de tu invalidación son la misma decisión, tomada en el momento en que eliges qué es una query y qué son sus argumentos. Por eso revalidar bien no empieza en la acción sino mucho antes, en cómo trazaste el mapa de queries: key frente a keyFor no son dos funciones que eliges al invalidar, son dos niveles de un árbol que dibujaste al nombrar. Y aquí cierra el arco entero del nivel. La identidad con clave hizo posible la deduplicación; hizo posible el prefetch, que deposita bajo una clave lo que otro leerá; hizo posible el optimismo, que proyecta envíos sobre datos con nombre; y hace posible la revalidación, que declara obsoleta una clave y recomputa exactamente su alcance. Un solo concepto —el dato como entidad con clave y alcance— sostiene los cinco patrones. 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: getTodos.key })y confirma que ahora solo se recarga la lista. - Edita un usuario y revalida con
getUsuario.keyFor(id); verifica que otros usuarios cacheados no se vuelven a pedir. - Usa
revalidateimperativo desde un botón “actualizar” fuera de toda acción y espera su promesa antes de mostrar un aviso de “listo”. - Cambia la lista a
createAsyncStoreconreconcileporidy comprueba que una revalidación que cambia una fila no remonta las demás.