Redirects, errores y revalidación desde una action
Una action termina de una de tres maneras, y cada una declara el siguiente estado del mundo. `throw redirect(path)` devuelve una respuesta con cabecera Location que el router sigue con una navegación, funcione o no el JavaScript. Los errores esperados se retornan como datos y se leen en `result`; los excepcionales se lanzan y caen en `error` o escalan a un ErrorBoundary. Y toda action revalida queries al terminar: por defecto todas, o acotadas con `json`, `reload` y `revalidate` usando `query.key` y `query.keyFor`.
Redirigir: throw redirect
redirect(url, init?) no navega por sí mismo: devuelve una respuesta —un Response con la cabecera Location apuntando al destino—. El router la reconoce y navega. Lo idiomático es lanzarla con throw, porque una redirección suele ser una salida temprana desde el fondo de la lógica —«si no hay sesión, fuera de aquí»— y throw corta en seco el resto de la función. También puedes retornarla; el router trata igual una respuesta lanzada que una devuelta.
import { action, redirect } from "@solidjs/router";const iniciarSesion = action(async (form: FormData) => { "use server"; const email = String(form.get("email") ?? ""); const clave = String(form.get("clave") ?? ""); const usuario = await autenticar(email, clave); if (!usuario) return { error: "Credenciales inválidas" }; // se queda y corrige await crearSesion(usuario); throw redirect("/panel"); // corta y navega}, "iniciarSesion");La virtud de que redirect sea una respuesta HTTP de verdad es que respeta el progressive enhancement sin esfuerzo. Con JavaScript, el router lee el Location y hace una navegación de cliente. Sin JavaScript, el envío nativo del formulario recibe un 302 y el navegador redirige por su cuenta —el mismo mecanismo que la web usa desde siempre—. Una sola línea cubre los dos mundos. Y admite ajustes: status (por defecto 302), headers, y un revalidate que estudiaremos enseguida.
throw redirect("/panel", { status: 303, revalidate: obtenerUsuario.key });El status merece un apunte fino. Tras un envío POST, redirigir con un 303 See Other es lo más correcto: instruye al navegador a seguir el destino con un GET, el patrón clásico POST-redirect-GET que evita que recargar la página reenvíe el formulario. El 302 por defecto funciona en la práctica para las navegaciones de cliente, pero cuando cuidas el comportamiento sin JavaScript, el 303 es el código que expresa con precisión «terminé de procesar tu envío, ahora ve a ver el resultado».
Errores: retornar lo esperado, lanzar lo excepcional
La lección de useSubmission estableció la dualidad; aquí la explotamos como estrategia de errores. Un error de validación —email mal formado, credenciales incorrectas, nombre duplicado— es un desenlace esperado del formulario: no es un fallo del sistema, es información para el usuario. Lo modelas retornando un valor, que aterriza en submission.result, y lo lees en la UI sin sobresalto.
import { Show } from "solid-js";import { useSubmission } from "@solidjs/router";function FormLogin() { const envio = useSubmission(iniciarSesion); return ( <form action={iniciarSesion} method="post"> <input name="email" type="email" required /> <input name="clave" type="password" required /> <Show when={envio.result?.error}> {(msg) => <p role="alert">{msg()}</p>} </Show> <button type="submit" disabled={envio.pending}>Entrar</button> </form> );}Para errores por campo, retorna una forma más rica —return { errores: { email: "…", clave: "…" } }— y pinta cada mensaje junto a su input. Reserva el throw para lo excepcional: la base de datos caída, un servicio externo que no responde, un invariante roto. Eso no es información para el usuario, es un fallo; lanzarlo lo deja en submission.error y, si no lo manejas ahí, escala al ErrorBoundary más cercano, que es justo donde deben tratarse los fallos genuinos.
Revalidación: por defecto todo, luego acotas
El detalle que cierra el círculo con las queries: toda action revalida al terminar. Por defecto, cuando la action retorna un valor normal, el router marca todas las queries como obsoletas y las vuelve a pedir. Es el comportamiento seguro —nunca te quedas con datos rancios— porque una mutación cualquiera podría haber afectado a cualquier lectura. Por eso el ejemplo optimista de la lección anterior funcionaba sin que pidieras nada: la creación de una nota revalidaba la lista sola.
Cuando esa red de seguridad es demasiado ancha —revalidar veinte queries cuando solo una cambió—, acotas. json(datos, { revalidate }) retorna datos y limita la revalidación a las claves que indiques; reload({ revalidate }) hace lo mismo cuando no necesitas retornar datos. Las claves salen de la query: query.key apunta a todos los argumentos cacheados de esa query, y query.keyFor(arg) a un juego concreto de argumentos.
import { action, json, reload } from "@solidjs/router";// Retorna el dato y revalida solo la lista de notasconst crearNota = action(async (form: FormData) => { "use server"; const nota = await db.notas.crear({ texto: String(form.get("texto") ?? "") }); return json(nota, { revalidate: obtenerNotas.key });}, "crearNota");// No retorna datos: solo revalida las tareas de un proyecto concretoconst marcarHecha = action(async (proyecto: string, id: string) => { "use server"; await db.tareas.completar(id); return reload({ revalidate: obtenerTareas.keyFor(proyecto) });}, "marcarHecha");Esas mismas claves sirven fuera de una action: la función revalidate(clave) fuerza la recarga de una query desde cualquier evento —un botón de refrescar, un mensaje de websocket que avisa de un cambio en el servidor— sin necesidad de mutar nada. Es el mismo mecanismo de invalidación que una action dispara al terminar, ahora en tus manos de forma imperativa: revalidate(obtenerNotas.key) marca la query como obsoleta y quien la esté leyendo con createAsync la vuelve a pedir dentro de una transición. Entre las tres formas —json y reload desde dentro de la mutación, revalidate desde fuera— cubres todos los momentos en que necesitas que los datos vuelvan a estar frescos.