useSubmission y useSubmissions: el estado de la mutación
Un envío de action no es fuego y olvido: el router lo materializa como un objeto reactivo con `pending`, `result`, `error`, `input`, `clear` y `retry`. `useSubmission` devuelve el último envío para pintar el estado de un formulario; `useSubmissions` devuelve todos los activos más un `pending` agregado, para operaciones concurrentes. La dualidad de salida gobierna dónde cae el resultado: lo que la action retorna aterriza en `result`, lo que lanza aterriza en `error`. Un filtro opcional acota qué envíos observa cada hook.
El envío como objeto reactivo
useSubmission(accion) devuelve un objeto que refleja el último envío de esa action y se actualiza de forma reactiva a lo largo de su ciclo de vida. Sus campos son el vocabulario con el que describes cualquier estado de formulario: pending es true mientras la mutación está en vuelo; result guarda el valor que la action retornó; error guarda la excepción que lanzó; input conserva los argumentos con que se disparó —para un formulario, un array cuyo primer elemento son los datos enviados—. Además trae dos acciones: clear() descarta el envío, y retry() lo relanza con el mismo input.
import { Show } from "solid-js";import { useSubmission } from "@solidjs/router";function FormNota() { const envio = useSubmission(crearNota); return ( <form action={crearNota} method="post"> <input name="texto" /> <button type="submit" disabled={envio.pending}> {envio.pending ? "Guardando…" : "Guardar"} </button> <Show when={envio.error}> {(err) => ( <p role="alert"> {err().message} <button type="button" onClick={() => envio.retry()}>Reintentar</button> <button type="button" onClick={() => envio.clear()}>Descartar</button> </p> )} </Show> </form> );}Fíjate en que no hay ningún createSignal, ningún try/catch en el componente, ningún estado manual de carga. Todo el estado del formulario —botón deshabilitado mientras guarda, mensaje de error, botones de reintento— es una lectura directa del objeto que useSubmission te entrega ya cableado al router.
Un matiz de ciclo de vida que ahorra sorpresas: el objeto de envío persiste tras completarse —con result o error ya poblados— hasta que otro envío lo reemplaza o llamas a clear(). Eso te permite seguir mostrando el desenlace después de que pending haya vuelto a false: un «Guardado» que se desvanece, un error que permanece a la vista. Y retry() no reconstruye nada: reejecuta la action con el input conservado, lo que lo vuelve ideal para un botón de reintento tras un fallo de red pasajero.
Retornar cae en result, lanzar cae en error
La dualidad más importante de este modelo es dónde aterriza cada salida de la action, porque define cómo la UI la trata. Lo que la action retorna —un valor, un objeto de errores de validación— aterriza en envio.result: es un desenlace esperado, parte del flujo normal. Lo que la action lanza —una excepción— aterriza en envio.error: es un desenlace excepcional. Esta separación no es un detalle de implementación; es una palanca de diseño que usas a conciencia.
const crearNota = action(async (form: FormData) => { "use server"; const texto = String(form.get("texto") ?? "").trim(); if (!texto) return { error: "El texto no puede estar vacío" }; // -> envio.result try { return await db.notas.crear({ texto }); // -> envio.result } catch { throw new Error("No se pudo guardar, intenta de nuevo"); // -> envio.error }}, "crearNota");La regla que se deriva: retorna los desenlaces que la UI debe pintar como parte del formulario —errores de validación campo a campo, un aviso de duplicado— y lanza los que son fallos genuinos —la base de datos caída, un permiso denegado—. Los primeros los lees en result sin sobresalto; los segundos los captura error, y si no los manejas ahí, escalan al ErrorBoundary más cercano. Verás esta distinción llevada al detalle en la lección de errores y redirects.
useSubmissions: varios envíos a la vez
useSubmission colapsa todo a un único envío, el más reciente, lo cual es perfecto para un formulario donde solo importa la operación en curso. Pero cuando disparas la misma action muchas veces sin esperar —borrar cinco filas de una lista, añadir varios ítems seguidos—, cada disparo es un envío distinto y todos coexisten en vuelo. Para eso está useSubmissions, que devuelve una colección iterable con todos los envíos activos, más un campo pending agregado que es true si cualquiera sigue pendiente.
import { Show, For } from "solid-js";import { useSubmissions } from "@solidjs/router";function BarraDeEstado() { const envios = useSubmissions(crearNota); const enVuelo = () => envios.filter((e) => e.pending).length; return ( <Show when={envios.pending}> <p aria-live="polite">Guardando {enVuelo()} nota(s)…</p> </Show> );}Cada elemento de la colección es un objeto de envío completo, con su propio pending, result, error e input. Esa colección es la materia prima del estado optimista de la próxima lección: recorrer los envíos en vuelo y pintar sus input como resultados provisionales.
Conviene precisar qué guarda input, porque la próxima lección lo exprime: es el array de argumentos con que se llamó a la action, en orden. Para un envío de formulario, input[0] son los datos enviados —el FormData o el URLSearchParams—. Si prefijaste con .with(id), ese id ocupa input[0] y los datos del formulario pasan a input[1]. Leer input es, entonces, reconstruir con exactitud la intención del usuario en el instante del envío, antes de que el servidor respondiera —justo lo que hace falta para dibujar un resultado que aún no está confirmado—.