wandres.dev
CREATERESOURCE · async como primitivo

refetch y mutate: recarga manual y actualización optimista

El segundo elemento de la tupla que devuelve createResource son dos palancas sobre el estado asíncrono. refetch re-ejecuta el fetcher bajo demanda —un botón de actualizar, un intervalo, una invalidación tras escribir— al margen de la fuente, y su argumento llega al fetcher como info.refetching. mutate escribe el valor local del resource sin tocar la red, el setter de su signal interno. Juntas construyen la actualización optimista: reflejar el resultado esperado antes de que el servidor confirme, y revertir si falla.

⏱ 16 min

Hasta ahora el resource ha sido reactivo pero pasivo: cargaba cuando la fuente cambiaba y punto. Un resource real necesita dos capacidades más. La primera es recargar bajo demanda, sin esperar a que la fuente cambie —porque el usuario pulsa «actualizar», porque un intervalo refresca, porque acabas de escribir en el servidor y sabes que los datos han quedado obsoletos—. La segunda es escribir su valor a mano, para que la interfaz responda al instante antes de que la red confirme nada. Esas dos capacidades viven en el segundo elemento de la tupla que devuelve createResource: refetch y mutate. Esta lección las presenta y las cruza en el patrón que las justifica a ambas —la actualización optimista—.

🎯 Al terminar esta lección sabrás
  • Recuperar refetch y mutate del segundo elemento que devuelve createResource.
  • Usar refetch para re-ejecutar el fetcher a mano, al margen del cambio de fuente.
  • Usar mutate para escribir el valor local del resource sin lanzar ninguna petición.
  • Construir una actualización optimista con reversión ante el error.

El segundo elemento: dos acciones sobre el resource

createResource devuelve una tupla. Hasta ahora solo has usado el primer elemento —el accessor con su ciclo de vida—, pero el segundo es un objeto con dos acciones que operan sobre el mismo resource.

const [datos, { refetch, mutate }] = createResource(fuente, fetcher);

refetch re-lanza la carga; mutate reescribe el valor. Una habla con el fetcher; la otra lo puentea. Entender esa diferencia —una re-deriva el valor desde su fuente de verdad, la otra lo fija localmente contra ella— es entender todo lo que sigue.

Ambas son funciones estables: no cambian de identidad entre renders —de hecho, en Solid ni siquiera hay renders que las recreen— así que puedes pasarlas a un manejador de eventos, a un componente hijo o a un temporizador sin envolverlas en nada ni preocuparte por dependencias. Pertenecen al resource y viven mientras él viva, lo que las hace cómodas de repartir por tu árbol sin las cautelas que otros modelos exigen alrededor de sus setters.

refetch: recargar sin que la fuente cambie

El fetcher corre solo cuando su fuente cambia. Pero a menudo necesitas recargar sin que nada haya cambiado: un botón de refresco, un setInterval que sondea, una invalidación tras una mutación. Para eso está refetch: re-ejecuta el fetcher con el valor actual de la fuente, cuando tú se lo pides.

const [feed, { refetch }] = createResource(traerFeed);

// un boton que recarga a mano
<button onClick={() => refetch()}>Actualizar</button>;

// o un refresco periodico, atado al ciclo de vida
const id = setInterval(refetch, 30_000);
onCleanup(() => clearInterval(id));

Puedes pasarle un argumento a refetch, y ese valor llega al fetcher como info.refetching. Es el canal para decirle a la carga por qué se la invoca —una recarga silenciosa de fondo, una forzada por el usuario— y ramificar en consecuencia.

const [feed, { refetch }] = createResource(async (_fuente, info) => {
  const silenciosa = info.refetching === "silenciosa";
  // ajusta el comportamiento segun el motivo de la recarga
  return traerFeed({ mostrarSpinner: !silenciosa });
});

refetch("silenciosa"); // info.refetching sera "silenciosa" en el fetcher

Cuando llamas a refetch y ya había un valor, el resource pasa a refreshing, no a pending: conserva el dato anterior en latest mientras la recarga viaja, justo el comportamiento que viste en la lección anterior. Esa continuidad es lo que hace del refresco periódico una experiencia agradable en vez de un parpadeo cada treinta segundos: el usuario sigue viendo el feed mientras la versión nueva llega por detrás, y solo se sustituye cuando está lista. refetch, además, devuelve la promesa del fetcher, así que puedes esperarla si necesitas encadenar algo justo después de que la recarga complete.

Un uso muy frecuente de refetch es la invalidación tras escribir: completas un formulario que crea o modifica algo en el servidor y, en lugar de adivinar cómo quedó la lista afectada, llamas a refetch sobre el resource que la carga para que vuelva a pedir la verdad. Es el patrón más honesto —releer en vez de simular— y el más simple de razonar, aunque cueste una ida y vuelta a la red que la mutación optimista, que veremos enseguida, se ahorra a cambio de asumir más responsabilidad.

mutate: escribir el valor sin pedirlo

mutate hace algo radicalmente distinto: escribe directamente el valor del resource, sin llamar al fetcher, sin tocar la red. Es, en esencia, el setter del signal interno del resource, y acepta tanto un valor nuevo como un actualizador que recibe el previo.

const [perfil, { mutate }] = createResource(traerPerfil);

// fija el valor local a mano, sin peticion alguna
mutate({ nombre: "Ada", rol: "admin" });

// o transforma el valor previo
mutate((p) => ({ ...p!, rol: "editor" }));

Tras un mutate, el resource queda en ready con el nuevo valor, y todo lo que lo lee reacciona de inmediato. No ha habido red; solo has movido el signal. Ese poder es exactamente lo que necesita la actualización optimista.

Merece la pena detenerse en la variante con actualizador, mutate((previo) => ...), porque es la que usarás casi siempre con datos estructurados. Recibe el valor actual y devuelve el nuevo, igual que el setter de un signal, lo que te permite transformar una lista o un objeto sin releerlo desde fuera del mutate. El ! que aparece en los ejemplos —previo!— reconoce que el valor podría ser undefined si el resource aún no ha resuelto; en un flujo optimista real, sueles saber que ya hay datos porque el usuario está interactuando con ellos, pero conviene tratar ese caso con honestidad en lugar de asumirlo.

No confundas mutate con refetch: mutate no dispara el fetcher ni consulta la fuente, solo escribe el signal. Si lo que quieres es el valor fresco del servidor, ese es trabajo de refetch. El malentendido típico —llamar a mutate esperando que recargue— produce interfaces que muestran datos inventados en lugar de reales; ten siempre presente cuál de las dos palancas re-consulta y cuál solo reescribe.

Como todo setter de Solid, mutate respeta la igualdad del signal subyacente: escribir un valor idéntico al actual no propaga nada. Y también como cualquier setter, cuando le pasas una función la interpreta como actualizador del valor previo, no como el valor en sí; almacenar de verdad una función dentro de un resource exigiría envolverla, un caso tan raro en datos de red que rara vez lo encontrarás, pero que conviene conocer para no llevarte una sorpresa.

La actualización optimista, con reversión

El uso canónico de mutate es la actualización optimista: reflejar en la interfaz el resultado esperado antes de que el servidor lo confirme, para que responda al instante, y deshacerlo si la operación falla.

async function alternarFavorito(articulo: Articulo) {
  const previo = articulo.favorito;
  // 1. optimismo: actualiza la UI ya, sin esperar al servidor
  mutate((lista) => marcar(lista!, articulo.id, !previo));
  try {
    // 2. confirma contra el servidor
    await fetch(`/api/favorito/${articulo.id}`, { method: "POST" });
  } catch {
    // 3. reversion: deshaz el cambio local si fallo
    mutate((lista) => marcar(lista!, articulo.id, previo));
  }
}

El patrón tiene tres tiempos: escribes el valor optimista con mutate, lanzas la petición real, y si rechaza vuelves a mutate con el valor anterior. Muchos equipos añaden un cuarto tiempo —un refetch tras el éxito— para reconciliar el estado local con la verdad del servidor, por si otra parte del sistema lo cambió mientras la petición viajaba.

La captura del valor previo antes de mutar es la pieza que hace posible la reversión, y es fácil de olvidar. Si no guardas el estado anterior antes de aplicar el optimismo, no tendrás a qué volver cuando el servidor rechace, y tu «reversión» estará adivinando. Por eso el esqueleto empieza siempre leyendo y reteniendo lo que vas a pisar. En interfaces con muchas mutaciones optimistas concurrentes, este patrón manual se vuelve repetitivo, y ahí es donde entran las abstracciones de más alto nivel de SolidStart —action y useSubmission— que industrializan exactamente este baile; pero todas se apoyan, por debajo, en las mismas dos operaciones que acabas de ver.

Hay una asimetría de responsabilidad que conviene aceptar con los ojos abiertos: la mutación optimista mejora la experiencia a cambio de que tú te encargues de la coherencia. El framework no sabe si tu predicción del resultado era correcta, así que confía en ti para revertir o reconciliar. Ese trato —más control a cambio de más responsabilidad— es el que separa una interfaz que se siente instantánea de una que solo espera, y es una elección deliberada, no un descuido del diseño.

flowchart TD
A[accion del usuario] --> M[mutate valor optimista]
M --> V[la UI reacciona al instante]
A --> P[peticion real al servidor]
P -->|exito| OK[opcional refetch reconcilia]
P -->|error| RB[mutate revierte al valor previo]
style M fill:#89b4fa,color:#11111b
style OK fill:#a6e3a1,color:#11111b
style RB fill:#f38ba8,color:#11111b
🔁

refetch bajo demanda

Re-ejecuta el fetcher al margen de la fuente. Su argumento viaja como info.refetching para distinguir el motivo de la recarga.

✍️

mutate local

Escribe el valor del resource sin red. Acepta un valor o un actualizador del previo, y deja el resource en ready al instante.

⚖️

Optimismo con red

Muta optimista, confirma contra el servidor, revierte si falla. El cuarto tiempo opcional es un refetch que reconcilia.

⚠️
mutate escribe un valor provisional: el siguiente fetch lo reemplaza

Recuerda que mutate escribe el valor local del resource: no persiste nada. La próxima vez que el fetcher corra —porque la fuente cambie o porque llames a refetch— tu valor mutado será reemplazado por lo que devuelva la red. Por eso el optimismo necesita su red de seguridad: la escritura optimista es provisional por diseño, y solo la confirmación del servidor —o un refetch que la lea— la vuelve definitiva. Tratar mutate como almacenamiento persistente es el malentendido que produce datos que «se revierten solos» al siguiente refresco, y depurarlo cuesta caro porque el síntoma aparece lejos de la causa.

refetch y mutate son las dos direcciones del flujo de datos asíncrono

Debajo de estas dos funciones hay una simetría que vale la pena hacer explícita, porque una vez la ves, no vuelves a confundir cuál usar. Todo estado asíncrono vive entre dos verdades: la del servidor, que es autoritativa pero lejana y lenta, y la del cliente, que es inmediata pero provisional. refetch y mutate son precisamente las dos flechas que conectan esas verdades, y apuntan en sentidos opuestos. refetch es la flecha que va del servidor al cliente: dice «olvida lo que crees saber y vuelve a preguntar a la fuente de verdad», y por eso re-ejecuta el fetcher y sobrescribe el valor local con lo que llegue. mutate es la flecha que va del cliente hacia sí mismo, en contra de la fuente: dice «asume este valor ya, aunque el servidor todavía no lo sepa», y por eso puentea el fetcher y escribe directo. La actualización optimista no es más que un baile coreografiado de estas dos flechas: mutate adelanta la verdad del cliente para que la interfaz no espere, la petición real intenta hacer que la verdad del servidor coincida, y según el resultado, o dejas la mutación en pie —quizá refrescándola con un refetch que la confirma— o la revocas con otro mutate. La razón por la que este patrón se siente tan natural en Solid, mientras que en otros modelos exige librerías enteras de gestión de estado servidor, es que aquí el resource ya es un signal: mutate es solo su setter y refetch solo su recarga, así que la reactividad de grano fino se encarga de propagar cada transición al DOM exacto que la necesita. No estás integrando dos sistemas —estado local y estado remoto—; estás moviendo un único signal en las dos direcciones que el problema admite. Cuando interiorizas que toda la complejidad del «estado servidor» se reduce a estas dos flechas sobre un signal, dejas de ver la carga de datos como un dominio aparte y la ves como lo que es: reactividad, con una fuente que a veces vive lejos.

⚔️ Domina las dos palancas
  1. Añade un botón «Actualizar» que llame a refetch y confirma que el resource pasa por refreshing conservando el valor previo en latest.
  2. Monta un setInterval que llame a refetch cada pocos segundos y límpialo con onCleanup; verifica que no se acumulan cargas al desmontar.
  3. Pasa un argumento a refetch y léelo como info.refetching en el fetcher para ramificar entre una recarga silenciosa y una visible.
  4. Implementa un mutate optimista con reversión en catch, y provoca a propósito un fallo del servidor para ver la interfaz volver al valor anterior.
  5. Tras un mutate optimista con éxito, dispara un refetch de reconciliación y razona qué bug evita cuando otro cliente cambió el mismo dato entretanto.