Next.js

Next.js Intercepting Routes Modal: Formularios SaaS Dashboard

Aprende a crear modales con intercepting routes en Next.js con enlaces profundos y estado usando parallel slots, Server Actions y shadcn/ui para dashboards SaaS.

By Mohamed DjoudirMay 19, 20269 min read
Compartir:
Next.js Intercepting Routes Modal: Formularios SaaS Dashboard
#nextjs-intercepting-routes-modal#next.js-parallel-routes#shadcn-ui#server-actions#app-router

Construir dashboards SaaS con gran carga de datos significa que los usuarios editan registros, ven perfiles o ajustan configuraciones con frecuencia. Al montar un diálogo estándar de React, atrapas al usuario: si refresca la página, comparte el enlace o presiona el botón de retroceso del navegador, pierde su contexto y el estado de tu aplicación se rompe. Para solucionar esto, los desarrolladores necesitan un modal de rutas interceptadas de Next.js robusto.

¿Qué es un modal de rutas interceptadas de Next.js?

Un modal de rutas interceptadas de Next.js es un patrón de enrutamiento que carga contenido de otra parte de la aplicación dentro del diseño (layout) actual, preservando el contexto y enmascarando la URL.

Cuando un usuario navega a /tasks/123, Next.js intercepta la solicitud y renderiza la interfaz de la tarea dentro de un slot de diseño paralelo por encima de la vista actual. Esto proporciona al usuario la experiencia de un diálogo rápido en el lado del cliente con los beneficios estructurales de una URL renderizada en el servidor.

Resuelve el problema clásico de las aplicaciones de una sola página (SPA) donde los modales carecen de enlaces compartibles y se rompen al presionar el botón de retroceso. Al vincular la superposición a un segmento de ruta real, los desarrolladores mantienen la capacidad de crear enlaces profundos (deep linking) sin sacrificar transiciones fluidas y con sensación nativa.

Por qué los modales tradicionales de React fallan en los dashboards SaaS

Históricamente, los desarrolladores front-end implementaban diálogos alternando una variable de estado booleana. Aunque es funcional para alertas o confirmaciones simples, este patrón colapsa en entornos complejos del App Router de Next.js.

Imagina un escenario donde un usuario filtra una tabla de datos de 500 filas, aplica parámetros de búsqueda complejos y hace clic en "Editar" en la fila 42. El usuario espera que la tabla permanezca intacta en el fondo. Si accidentalmente presiona el botón de retroceso para cerrar el modal, un diálogo basado en estado booleano lo sacará completamente del dashboard. Además, los agentes de soporte no pueden compartir un enlace directo a ese registro específico porque la URL nunca cambió.

Migrar a una arquitectura impulsada por URL se integra de forma nativa con el historial del navegador. Comparemos ambos enfoques directamente.

Característica Diálogo tradicional useState Rutas interceptadas
Botón atrás del navegador Sale de toda la aplicación dashboard Cierra de forma segura el modal
Compartir URL directa Imposible (requiere mapeo manual) Nativo (navega a la página independiente)
Refrescar página Reinicia el estado de la aplicación Renderiza la vista específica del registro
Contexto de fondo No afectado visualmente, pero frágil en historial Preservado nativamente vía rutas paralelas
Impacto en rendimiento Incluye código del modal en el chunk principal División de código automática por segmento

Next.js Parallel Routes vs Intercepting Routes: ¿Cuál es la diferencia?

Construir un diálogo impulsado por URL requiere que dos características distintas de Next.js trabajen juntas: rutas paralelas y rutas interceptadas. Aunque a menudo se mencionan indistintamente, cumplen propósitos totalmente diferentes en el framework.

Las rutas paralelas de Next.js utilizan slots con nombre, prefijados con el símbolo @. Estos slots te permiten renderizar múltiples páginas en el mismo layout simultáneamente. Cuando el usuario navega, Next.js puede actualizar el slot paralelo sin volver a renderizar o perder el estado del slot primario children.

Sin embargo, una ruta paralela por sí sola solo reserva espacio estructural. También necesitamos "secuestrar" la intención de enrutamiento. Aquí es donde entran las rutas interceptadas. Usando convenciones de carpetas como (.), (..), (..)(..), o (...), le indicas a Next.js que intercepte un evento de navegación y renderice el contenido de destino en un slot específico.

Definición de las convenciones del sistema de archivos

La carpeta de interceptación debe reflejar el nivel relativo del segmento de destino en la jerarquía de rutas.

  • (.) coincide con segmentos en el mismo nivel.
  • (..) coincide con segmentos un nivel arriba.
  • (..)(..) coincide con segmentos dos niveles arriba.
  • (...) coincide con segmentos desde la raíz del directorio app.

Aquí tienes una estructura de carpetas lista para producción que mapea rutas estándar y sus contrapartes interceptadas.

app/
├── layout.tsx
├── @modal/                   // El slot de ruta paralela
│   ├── default.tsx           // Fallback para slots sin coincidencia
│   └── (.)tasks/             // Interceptando ruta al mismo nivel
│       └── [id]/
│           └── page.tsx      // El componente UI del modal
└── tasks/
    ├── page.tsx              // La vista de fondo primaria
    └── [id]/
        └── page.tsx          // Vista de página completa independiente

En este árbol, (.)tasks/[id] intercepta la navegación suave hacia /tasks/[id]. Debido a que se encuentra dentro del slot @modal, el componente interceptado se renderiza junto a tasks/page.tsx.

Construyendo un modal de edición compartible con shadcn/ui Dialog

Integrar una biblioteca de UI como shadcn/ui requiere un puente arquitectónico específico. Radix UI, que impulsa las primitivas de diálogo subyacentes, espera controlar su propio estado de visibilidad. Debemos invertir este control para que el enrutador de Next.js dicte cuándo se monta y desmonta el modal.

Debido a que las rutas interceptadas solo se aplican a la navegación suave (hacer clic en un <Link> de Next.js), nuestro diálogo debe abrirse automáticamente al montarse. Cuando el usuario interactúa con la UI para cerrar el modal, usamos el router para descartar la superposición.

Creando el componente envoltorio del modal

Debes establecer defaultOpen={true} y sobrescribir el manejador onOpenChange. Si el diálogo intenta cerrarse, interceptamos ese evento y disparamos el recorrido del historial. Esto saca un elemento del historial del navegador en lugar de simplemente ocultar visualmente los elementos del DOM.

"use client";

import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { ReactNode } from "react";

interface InterceptedModalProps {
  children: ReactNode;
  title: string;
}

export function InterceptedModal({ children, title }: InterceptedModalProps) {
  const router = useRouter();

  const handleOpenChange = (isOpen: boolean) => {
    if (!isOpen) {
      // Sale limpiamente de la ruta interceptada recorriendo el historial
      router.back();
    }
  };

  return (
    <Dialog defaultOpen={true} onOpenChange={handleOpenChange}>
      <DialogTitle className="sr-only">{title}</DialogTitle>
      <DialogContent className="sm:max-w-[600px]">
        {children}
      </DialogContent>
    </Dialog>
  );
}

Manejo de la navegación dura: El fallback default.tsx

El App Router de Next.js aplica reglas de reconciliación estrictas en torno a los slots de rutas paralelas. Cuando un usuario refresca el navegador o pega una URL directamente en la barra de direcciones, el framework omite el interceptor por completo y renderiza la vista de página completa independiente.

En Next.js 16, los slots de rutas paralelas necesitan un fallback default.tsx cuando el slot puede no tener coincidencia en una navegación dura. La documentación oficial señala que se utiliza un archivo default.tsx como fallback cuando Next.js no puede recuperar el estado activo de un slot tras una carga completa. No proporcionar este archivo para un slot afectado provoca un error de compilación.

El archivo default.tsx

Un archivo default.tsx que retorna null dentro del slot @modal asegura que el slot permanezca oculto cuando ninguna ruta interceptada coincide.

// app/@modal/default.tsx
export default function ModalDefault() {
  // Retorna null para asegurar que la superposición permanezca invisible 
  // cuando ninguna ruta interceptada coincide activamente con la URL.
  return null;
}

Gestión de mutaciones de formularios y Server Actions dentro del modal

Enviar una Server Action dentro de un modal interceptado requiere sumo cuidado. Usar revalidatePath directamente puede reiniciar el slot del layout prematuramente.

En Next.js 16, el enfoque moderno para la consistencia inmediata es la API updateTag. Mientras que revalidateTag(tag, "max") se usa para actualizaciones en segundo plano (stale-while-revalidate), updateTag(tag) está diseñado para escenarios de "lectura de tus propias escrituras" (read-your-own-writes), como los envíos de formularios donde el usuario espera un refresco instantáneo.

Cierre seguro tras el envío

Combina una Server Action con el recorrido del historial en el cliente usando el hook useActionState de React 19.

"use client";

import { useActionState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { updateTaskAction } from "@/actions/tasks";
import { Button } from "@/components/ui/button";

export function EditTaskForm({ taskId, initialData }: { taskId: string, initialData: string }) {
  const router = useRouter();
  const [state, formAction, isPending] = useActionState(updateTaskAction, null);

  useEffect(() => {
    if (state?.success) {
      // La mutación tuvo éxito; cerrar modal vía retroceso de historial
      router.back();
    }
  }, [state?.success, router]);

  return (
    <form action={formAction} className="space-y-4">
      <input type="hidden" name="id" value={taskId} />
      <div className="flex flex-col gap-2">
        <label htmlFor="title" className="text-sm font-medium">Task Title</label>
        <input 
          id="title"
          name="title" 
          defaultValue={initialData} 
          className="border p-2 rounded-md"
        />
      </div>
      <Button type="submit" disabled={isPending}>
        {isPending ? "Saving..." : "Save Changes"}
      </Button>
    </form>
  );
}

Dentro de tu Server Action, usa updateTag('tasks') para que el caché expire inmediatamente. Esto asegura que la próxima solicitud espere por datos frescos.

// actions/tasks.ts
"use server";

import { updateTag } from "next/cache";

export async function updateTaskAction(prevState: any, formData: FormData) {
  // ... lógica de base de datos ...
  
  // updateTag está diseñado para consistencia inmediata en acciones
  updateTag("tasks");
  
  return { success: true };
}

Errores comunes

  • Usar Route Groups en lugar de interceptores: Los nombres de carpetas como (modal) son Route Groups. Debes usar (.), (..), o (...) para indicar al enrutador que intercepte la navegación.
  • Olvidar el fallback default.tsx: Next.js 16 requiere esto para slots paralelos sin coincidencia en cargas completas; omitirlo provoca errores de compilación.
  • revalidateTag con un solo argumento: Esta firma está obsoleta en Next.js 16. Usa revalidateTag(tag, "max") para SWR o updateTag(tag) para invalidación inmediata en acciones.
  • Omitir defaultOpen={true}: Sin esto, los diálogos basados en Radix se montan pero permanecen invisibles, bloqueando la UI de fondo.

FAQ

¿Qué es una ruta interceptada en Next.js?

Una ruta interceptada permite cargar contenido de otra parte de tu aplicación dentro del diseño actual preservando el contexto. Las convenciones como (.) o (..) definen cuántos segmentos subir en la jerarquía para coincidir.

¿Cómo cierro un modal en las rutas interceptadas de Next.js?

Usa router.back() de next/navigation. Dado que el modal está vinculado a un segmento de ruta, retroceder en la pila del historial desmonta el slot y restaura la URL anterior.

¿Cuál es el propósito de default.tsx?

El archivo default.tsx actúa como un fallback para los slots de rutas paralelas durante una navegación dura (como refrescar la página) cuando Next.js no puede determinar el estado activo de ese slot.

¿Cuándo debo usar updateTag vs revalidateTag?

Usa updateTag(tag) dentro de Server Actions para consistencia inmediata (lectura de tus propias escrituras). Usa revalidateTag(tag, "max") para actualizaciones en segundo plano donde servir contenido antiguo temporalmente es aceptable por rendimiento.

Conclusión

Dominar los modales impulsados por URL transforma la forma en que los usuarios interactúan con tus dashboards. Al combinar slots @modal, convenciones de interceptación y recorrido programático del historial, proporcionas una capacidad de enlaces profundos sin sacrificar la sensación fluida de una SPA.

Si quieres saltarte el código base y entregar interfaces de cliente más rápido, explora nuestras plantillas de dashboard listas para producción. Vienen preconfiguradas con arquitecturas de enrutamiento de Next.js 16, formularios de React 19 y sistemas de diseño robustos de fábrica.

¿Encontró útil este artículo?

Compartir:
world map

Comunidad Global de Usuarios

Únete a miles de desarrolladores en todo el mundo que confían en Aniq-UI para sus proyectos. Nuestras plantillas se utilizan en todo el mundo para crear experiencias web impresionantes.

Contáctanos

Need custom work or reskin? Get in touch with us

Aniq-uiAniq-uiAniq-ui