Mutaciones optimistas con action y useSubmission
Actualizar la interfaz antes de que el servidor confirme, y revertir si falla. Cómo `action` modela una mutación con nombre, cómo `useSubmission` y `useSubmissions` exponen los envíos en vuelo con su entrada, su estado pendiente y su error, cómo pintar el estado optimista a partir de esos envíos, y por qué el rollback ante un fallo es automático: al dejar de estar pendiente, la interfaz vuelve a leer la verdad del servidor sin código de deshacer.
action: una mutación con nombre
Igual que query da identidad a una lectura, action da identidad a una escritura. Envuelves la función que muta el servidor y le das un nombre; a cambio, el framework la vincula al ciclo de envíos que luego observarás.
// data/todos.tsimport { action, query } from "@solidjs/router";export const getTodos = query(async () => { return (await (await fetch("/api/todos")).json()) as Todo[];}, "todos");export const addTodo = action(async (texto: string) => { const res = await fetch("/api/todos", { method: "POST", body: JSON.stringify({ texto }), }); if (!res.ok) throw new Error("no se pudo crear"); return (await res.json()) as Todo;}, "addTodo");La disparas de dos maneras. Con un formulario, action={addTodo} da mejora progresiva —funciona incluso sin JavaScript—. O de forma imperativa con useAction, que te devuelve una función normal a la que pasas argumentos tipados. Usaremos la segunda porque deja la entrada del envío en una forma cómoda de pintar.
import { useAction } from "@solidjs/router";const agregar = useAction(addTodo);// agregar("Comprar pan") dispara la mutacionLeer los envíos en vuelo
El corazón del patrón es que cada disparo de una acción genera un envío observable. useSubmission te da el último; useSubmissions, la colección de todos los que están en vuelo o acaban de completarse. Cada envío expone input —los argumentos con que se llamó—, pending, result y error.
import { useSubmissions } from "@solidjs/router";const enviando = useSubmissions(addTodo);// enviando.pending -> hay alguno en vuelo// [...enviando] -> lista de envios, cada uno con input, pending, result, errorUn envío pendiente de addTodo guarda en input[0] el texto que le pasaste. Esa es la materia prima del optimismo: ya sabes lo que el usuario quiere crear antes de que el servidor lo confirme, así que puedes pintarlo ya.
Elige el hook según la cardinalidad. useSubmission —singular— sigue el último envío y basta cuando la interacción es de uno en uno: un botón de guardar, un formulario que se bloquea mientras viaja. useSubmissions —plural— reúne todos los envíos vivos y es el que necesitas cuando el usuario puede encolar varios sin esperar, como añadir tres tareas seguidas. Su pending agregado, además, es la señal natural para deshabilitar controles mientras algo está en curso.
const envio = useSubmission(addTodo);<button disabled={envio.pending}>Guardar</button> // se bloquea mientras viajaPintar el estado optimista
La lista real vive en getTodos a través de createAsync. Junto a ella, mapeamos los envíos pendientes de addTodo como filas “fantasma”, pintadas con la entrada del propio envío. El usuario ve su tarea aparecer al instante, atenuada mientras viaja.
import { createAsync, useAction, useSubmissions } from "@solidjs/router";import { For, Show, createSignal } from "solid-js";function ListaTodos() { const todos = createAsync(() => getTodos()); const agregar = useAction(addTodo); const enviando = useSubmissions(addTodo); const [texto, setTexto] = createSignal(""); return ( <> <button onClick={() => agregar(texto())}>Anadir</button> <ul> <For each={todos()}>{(t) => <li>{t.texto}</li>}</For> <For each={[...enviando].filter((s) => s.pending)}> {(s) => <li class="fantasma">{s.input[0]}</li>} </For> </ul> </> );}Cuando la acción resuelve y la lista se revalida (lo verás en la siguiente lección), la fila real llega desde el servidor y el envío deja de estar pendiente: la fila fantasma desaparece justo cuando la verdadera ocupa su sitio. El relevo es invisible.
Revertir si falla: el rollback es automático
Aquí está la elegancia del modelo. No escribes lógica de deshacer. Si la acción lanza un error, ese envío deja de estar pendiente —pasa a tener error— y, como tu fila fantasma solo se pinta para envíos con pending verdadero, se desvanece sola. La interfaz revierte a la única fuente de verdad que le queda: la lista del servidor, que nunca llegó a incluir la tarea fallida. El estado optimista era una capa derivada de los envíos vivos; al morir el envío, la capa se retira sin dejar rastro.
import { useSubmission } from "@solidjs/router";const envio = useSubmission(addTodo);// Muestra el fallo y ofrece reintentar sin tocar la lista real<Show when={envio.error}> <p role="alert">No se pudo guardar. Reintenta.</p></Show>Puedes además llamar a envio.clear() para descartar un envío completado o fallido y limpiar su rastro del historial de submissions. Pero lo esencial es el principio: la verdad vive en el servidor; el optimismo es una proyección temporal de los envíos sobre esa verdad, y desaparece en cuanto el envío se resuelve, con éxito o con fracaso.