4dim / Notas

redirect() en una Server Action recarga la página entera

Marcar un favorito hacía parpadear toda la bandeja: la acción terminaba en redirect() hacia la misma URL, y eso desmonta y vuelve a montar el árbol de React. La alternativa, la trampa de useOptimistic con un refresco lento, y los 35 enlaces que recargaban el documento.

El parpadeo

Si tu equipo pasa el día dentro de una herramienta web, esta nota explica por qué a veces la pantalla parpadea y cómo se quita. Nos pasó en la consola del CRM, el programa donde se atienden las conversaciones con clientes.

En la bandeja, marcar una conversación como favorita hacía parpadear la pantalla entera. Encender el agente de IA, igual. Donde más se notaba era al enviar un mensaje, porque ahí se escribe veinte veces seguidas y cada envío recargaba la conversación completa.

Nada fallaba. Solo parpadeaba. Y en una herramienta de trabajo, el parpadeo es fatiga.

Por qué: una navegación completa hacia la misma URL

Todas esas acciones eran Server Actions de Next, funciones que corren en el servidor cuando alguien pulsa un botón. Y todas terminaban en redirect() hacia la misma página, con la intención de «refrescar».

Pero redirect(), llamado desde una Server Action, provoca una navegación completa del documento. Desmonta el árbol de React entero y lo vuelve a montar, aunque el destino sea la URL en la que ya estabas. El parpadeo era ese desmontaje.

Lo detectamos y corregimos cuatro veces en sitios distintos antes de escribir la regla en el código: ninguna acción de estado usa redirect(). redirect() es para ir a otro sitio, no para quedarse.

La alternativa: revalidatePath y estado local

La acción hace su trabajo en el servidor y llama a revalidatePath, que le pide a Next volver a generar los datos de esa ruta. React compara lo nuevo con lo que ya hay en pantalla y cambia solo lo que cambió, sin desmontar nada.

Lo que se ve al instante lo pinta el navegador por su cuenta, con estado optimista: el corazón se enciende al hacer clic, dando por hecho que la acción saldrá bien, y la acción solo lo confirma después.

Para enviar un mensaje fuimos más lejos. La confirmación del propio mensaje llega por el mismo canal en tiempo real que trae los mensajes del cliente. Un solo mecanismo para «lo mío» y «lo suyo», en vez de dos caminos que hay que mantener de acuerdo.

La trampa de useOptimistic

React 19 trae useOptimistic justo para esto, con una condición que la documentación no subraya. Al terminar la transición descarta el valor optimista y vuelve a los props, que son los datos que el componente recibe de fuera.

Si los props todavía no traen el dato nuevo, el botón revierte en pantalla aunque la acción haya salido bien en la base de datos. Eso pasaba porque el refresco de respaldo de la bandeja corre cada 45 segundos. Se encendía, se apagaba solo, y el usuario creía que no había funcionado.

Encima había un segundo fallo. Un reindentado había cambiado por accidente la clave que se leía, «favorita» donde se guardaba «favorito». Como !undefined es siempre verdadero, el botón solo sabía encender.

Se corrigió con estado local propio, que manda hasta que el servidor diga lo contrario. Y con los dos «sellos» en una sola tabla que se recorre, para que el dibujo y el manejador del clic lean el mismo campo y no puedan discrepar.

El estado optimista solo es optimista si nadie lo pisa antes de que llegue la confirmación. Con un refresco lento, useOptimistic lo pisa.

El mismo síntoma, otra causa. Toda la consola navegaba con <a href> normales, y cada clic era una navegación completa. Convertimos 35 enlaces en 22 archivos a <Link> de Next con un script, y el script dejó dos lecciones.

La primera: se saltó los enlaces cuya ruta venía de una variable, porque buscaba rutas escritas literalmente. Esos siguieron recargando hasta otro commit.

La segunda es más fina. Su expresión regular para insertar el import no llevaba la bandera m, así que ^ solo casaba el inicio del archivo. En los que empiezan con "use client" no encontró dónde insertar, y no insertó. Queda un ReferenceError en tiempo de ejecución, y el build de Next no lo detecta.

Un detalle final. En una consola donde cada columna tiene su propio scroll, <Link> lleva scroll={false}. Si no, cada navegación devuelve el documento arriba y pelea con el componente que baja el hilo al último mensaje.

Qué hacer entonces

  • Buscar redirect( en las Server Actions. Cada una que apunte a la misma página es un parpadeo.
  • revalidatePath para los datos, estado local para lo inmediato.
  • Con useOptimistic, medir cuánto tardan los props en traer el dato. Si tardan más que la transición, no sirve.
  • Buscar <a href en la app. Y después de un codemod, buscar lo que el codemod no vio: rutas en variables, archivos con directiva arriba.
  • Recordar que el build no detecta un ReferenceError. Probar navegando.

Esta nota sale de nuestro trabajo en Desarrollo web.

← todas las notas