Reglas de Navegación y Enrutamiento
Convenciones de enrutamiento basado en archivos e higiene de enlaces profundos para Expo Router en Expo SDK 57. Estas reglas mantienen la navegación predecible a medida que crece el árbol app/, previenen errores de autenticación y parámetros de URLs mal formadas, y garantizan que los compilaciones de tiendas manejen enlaces universales correctamente en iOS y Android.
- Aplica Nivel 1 al crear la estructura de
app/ o agregar un nuevo grupo de rutas - los errores de layout son caros de deshacer.
- Revisa Niveles 2–3 en cada PR que toque
app/, Link, router.push, o configuración de scheme en app.config.
- Ejecuta Nivel 4 antes de la presentación en la tienda - los enlaces profundos y redirecciones de autenticación son fuentes de fallos principales en análisis de producción.
- Empareja las reglas de validación de parámetros con pruebas de contrato de Jest - lint no puede probar que un enlace profundo mal formado sea rechazado.
- Documenta patrones de navegación no estándar (modales como rutas vs superposiciones) en un ADR antes de que el tercer equipo los copie.
-
Los archivos de ruta re-exportan pantallas de características: app/(tabs)/orders/index.tsx importa OrdersScreen desde features/orders - sin hooks, llamadas fetch, o Redux en archivos de ruta.
- Objetivo: Archivos de ruta bajo 20 líneas; la lógica vive en módulos de características testeables.
- Rechaza: Pantallas de 200 líneas cometidas directamente bajo
app/.
-
Un solo _layout.tsx raíz posee proveedores globales: Tema, sesión de autenticación, cliente de consultas, y límites de error se montan una sola vez en app/_layout.tsx - no repetidos en cada layout anidado.
- Orden: Splash → fuentes → proveedores →
<Slot /> o stack - documenta la secuencia en README.
- Rechaza:
QueryClientProvider duplicado en layouts de tab y stack.
-
Los layouts anidados manejan solo la interfaz: El _layout.tsx de tab define iconos y barra de pestañas; el _layout.tsx de stack establece headerShown y transiciones - no obtención de datos de características.
- Patrón: La actualización de
useFocusEffect pertenece al componente de pantalla, no a _layout.tsx.
- Excepción: Layouts de compuerta de autenticación que redirigen usuarios no autenticados - mantén la lógica de redirección mínima.
-
Usa grupos de rutas para organización, no comportamiento en tiempo de ejecución: (auth), (tabs), y (modals) agrupan archivos sin afectar la ruta de URL - las carpetas entre paréntesis son para estructura.
- Nombres: Los nombres de archivos en kebab-case minúsculas coinciden con segmentos de URL -
order-detail.tsx → /order-detail.
- Rechaza: Anidamiento profundo puramente para reflejar el organigrama (
app/team-a/feature-b/...).
-
Rutas de índice para centros de listas; segmentos dinámicos para entidades: orders/index.tsx lista órdenes; orders/[id].tsx muestra una orden - las URLs de estilo REST predecibles ayudan con enlaces profundos y análisis.
- Captura todo: Reserva
[...slug].tsx para soporte CMS o URLs heredadas - valida segmentos inmediatamente.
- Rechaza: IDs opacos en query strings cuando un segmento de ruta es más claro (
/orders/123 no /orders?id=123).
-
Coloca la UI de carga y error con rutas cuando uses Suspense boundaries: orders/[id].tsx puede exportar un orders/[id]/loading.tsx paralelo o manejar esqueletos en la pantalla - no dejes destellos en blanco durante la navegación.
- Patrón: La pantalla de características acepta prop
isLoading; la ruta cableará suspense si se adopta.
- Prueba: RNTL
renderRouter para stacks críticos.
-
Prefiere objetos href sobre rutas de cadena: router.push({ pathname: '/orders/[id]', params: { id } }) - las rutas tipadas atrapan errores tipográficos en tiempo de compilación cuando experiments.typedRoutes está habilitado.
- Habilita:
experiments: { typedRoutes: true } en app.config para proyectos SDK 57.
- Rechaza:
router.push('/orders/' + id) sin validación de parámetros.
-
Usa Link para navegación declarativa; router para imperativa: Link preserva semántica de accesibilidad y prefetch - usa router.replace imperativo solo después de mutaciones o redirecciones de autenticación.
- Stack de atrás:
router.replace después de login/logout - push duplica pantallas de autenticación en gesto de atrás.
- Rechaza:
router.push dentro de onPress cuando Link con asChild y Pressable es suficiente.
-
Centraliza constantes de ruta en un módulo: routes.ts exporta constructores href - las características importan orderDetailHref(id) en lugar de dispersar cadenas de ruta.
- Monorepo:
packages/navigation compartido para aplicaciones multi-marca con formas de ruta idénticas.
- Rechaza:
'/settings/account' copiado en doce archivos.
-
Modales y rutas de presentación son explícitos: Los modales de pantalla completa obtienen un grupo (modals) o presentation: 'modal' en opciones de stack - las superposiciones semi-implementadas confunden el comportamiento de atrás en Android.
- Android: Prueba atrás en hardware en cada ruta modal - debe descartar el modal, no salir de la aplicación.
- ADR: Documenta cuándo usar React Native
Modal vs una ruta modal basada.
-
Protege rutas autenticadas en layout, no en guardias por pantalla: (app)/_layout.tsx verifica la sesión y redirige a /(auth)/login - las pantallas individuales no deben duplicar cada una verificaciones de autenticación.
- Sesión obsoleta: Escucha la expiración del token en el nivel de layout y llama a
router.replace.
- Rechaza:
if (!user) return null en cada pantalla protegida sin redirección.
-
Maneja la URL inicial y enlaces profundos de inicio en frío una sola vez: useURL() o la configuración de enlace de Expo Router resuelve la URL de lanzamiento - no analices Linking.getInitialURL() en cada pantalla.
- Patrón: El layout de autenticación lee el enlace profundo pendiente después de login y navega a la ruta almacenada.
- Prueba: Flujo de Maestro abriendo
myapp://orders/42 desde fuera de la aplicación.
-
Declara scheme en app.config antes de distribuir esquemas de URL personalizados: scheme: "acme" habilita enlaces acme:// - debe coincidir con la documentación de marketing y QA.
- Múltiples esquemas: Usa
scheme: ["acme", "acme-dev"] para variantes de entorno - no un esquema para todas las compilaciones.
- Verifica:
npx uri-scheme list después de prebuild.
-
Configura dominios asociados para enlaces universales (iOS) y app links (Android): ios.associatedDomains e android.intentFilters en app.config - editar archivos nativos a mano rompe en prebuild CNG.
- Plugin de configuración: Usa el plugin
expo-router y plugins de dominio asociado documentados.
- Rechaza: Distribuir enlaces universales sin
apple-app-site-association alojado y validado.
-
Valida parámetros de ruta en el límite de características: Zod (o similar) analiza useLocalSearchParams() antes de renderizar - los enlaces profundos mal formados muestran un error amigable, no un redbox.
- Ejemplo:
const { id } = orderParamsSchema.parse(params) en OrderDetailScreen.
- CI: Pruebas de contrato para esquemas de parámetros - no E2E para cada combinación de parámetros.
-
Nunca confíes en la entrada de query string para decisiones de seguridad: El enlace profundo ?admin=true no otorga admin - autoriza en el servidor y en el estado de sesión.
- Desinfecta: Elimina parámetros desconocidos; registra cargas rechazadas en staging.
- Rechaza:
if (params.promo) aplicando descuentos sin validación del servidor.
-
Documenta la matriz de enlaces profundos soportados en README: Ruta, parámetros requeridos, requisito de autenticación y URL de ejemplo por ruta - soporte y marketing dependen de esta tabla.
- Versión: Bump de matriz cuando las rutas se renombran - los enlaces rotos sobreviven en campañas de email durante años.
- Herramientas: Genera matriz desde tipos de ruta donde sea posible.
-
Ruta de fallback para rutas desconocidas: [...unmatched].tsx o +not-found.tsx muestra UI recuperable con un enlace al inicio - el 404 predeterminado causa redbox y daña opiniones de tienda.
- Análisis: Registra rutas sin coincidencia para detectar campañas rotas.
- Prueba: Abre
myapp://does-not-exist en humo de Maestro.
-
Carga perezosa de pantallas de tabs pesadas cuando las pestañas se visitan raramente: import() dinámico para sub-pantallas de configuración o herramientas de administración - no para tabs de ingresos principales que los usuarios abren cada sesión.
- Mide: Analizador de bundle antes de splits perezosos - los splits prematuros agregan latencia.
- Rechaza: Carga perezosa de la ruta de primera pintura de tab inicio.
-
Evita el estado de navegación en singletons globales mutables: Pasa parámetros a través de router o stores de características - el módulo-nivel let pendingOrderId se rompe en navegaciones rápidas y recargas OTA.
- Solución: Cache de TanStack Query con clave por parámetro de ruta, o parámetros de ruta como fuente de verdad.
- Olor:
global.pendingDeepLink establecido en un archivo, leído en otro.
-
Reinicia el stack en logout: router.dismissAll() o reemplaza al stack de autenticación - el gesto de atrás no debe devolver a pantallas autenticadas con un token borrado.
-
No bloquees la primera pintura en la carga de fuentes/iconos de navegación: Carga iconos de tab desde activos estáticos; aplaza rutas de fuentes personalizadas hasta que el layout raíz termine useFonts.
- Patrón: El layout raíz devuelve
null hasta que las fuentes se cargan - las rutas secundarias no duplican compuertas de fuentes.
- Rechaza:
if (!fontsLoaded) return null a mitad de orden-de-hooks en una pantalla (violación de regla hooks).
-
El humo E2E cubre tres rutas de enlaces profundos: Lanzamiento, ruta con compuerta de autenticación, y detalle de entidad primaria - YAML de Maestro contra compilaciones de vista previa de EAS, no solo Expo Go.
- Fija:
appId al identificador de bundle desde app.config.
- CI: Ejecuta en candidatos de lanzamiento en dispositivos físicos.
-
Lista de verificación PR para cambios de enrutamiento: ¿Archivo renombrado? ¿Matriz de enlaces profundos actualizada? ¿Prueba de esquema de parámetros? ¿Atrás en hardware en modales? - adjunta a la plantilla PR de enrutamiento.
- Cambio de ruptura: El renombre de ruta es una API de ruptura para enlaces de marketing - versiona o redirige rutas antiguas.
- Nota OTA: Las adiciones de ruta solo-JS son seguras para OTA; los cambios de scheme requieren una compilación de tienda.
- Nivel 1 (1–6): Estructura de archivos y layouts - establece antes de agregar características; mover pantallas más tarde rompe marcadores y análisis.
- Nivel 2 (7–12): API de navegación - previene rutas tipadas como cadenas y lógica de autenticación duplicada.
- Nivel 3 (13–18): Enlaces profundos - requerido antes de que las campañas externas y enlaces universales se pongan en vivo.
- Nivel 4 (19–24): Rendimiento y verificación - campañas de puerta y promociones OTA de candidatos de lanzamiento.
¿Debe la lógica de negocio vivir en app/ o features/?
Siempre en features/ - app/ es una tabla de enrutamiento. Los archivos de ruta importan y re-exportan pantallas; no obtienen datos ni poseen hooks más allá del cableado trivial.
¿Cómo habilito rutas tipadas en SDK 57?
Establece experiments: { typedRoutes: true } en app.config y usa objetos href con router.push y Link. Ejecuta npx expo customize tsconfig.json si la plantilla aún no lo ha hecho.
¿Dónde pertenecen las redirecciones de autenticación?
En un layout dedicado ((app)/_layout.tsx o (auth)/_layout.tsx) que verifica la sesión una sola vez y llama a router.replace. Evita redirecciones useEffect por pantalla que causen carrera en inicio en frío.
¿Esquema personalizado vs enlaces universales?
Los esquemas personalizados (acme://) son más fáciles de probar pero pueden ser secuestrados en algunas versiones de Android. Los enlaces universales/app (https://app.acme.com/...) son requeridos para email y campañas web-a-app - configura ambos para aplicaciones de producción.
¿Cómo valido parámetros de enlaces profundos?
Analiza useLocalSearchParams() con Zod en la parte superior de la pantalla de características. Muestra un estado 404 o error en fallo. Agrega pruebas de Jest para el esquema - no Maestro para cada parámetro inválido.
¿Modal como ruta o React Native Modal?
Los modales basados en rutas participan en enlaces profundos y atrás en Android - prefíerelos para flujos compartibles. RN Modal está bien para selectores efímeros sin representación de URL. Documenta la opción en un ADR.
¿Puedo renombrar un archivo de ruta después del lanzamiento?
Sí en JS vía OTA, pero las URLs antiguas se rompen a menos que agregues redirecciones en [...unmatched].tsx o mantengas archivos de alias. Trata rutas como una API pública - versiona cambios de ruptura.
¿Cuántos archivos _layout.tsx es demasiado?
Uno raíz más uno por navegador principal (tabs, auth stack, modales group) es típico. Más de cuatro layouts anidados generalmente significa que el árbol debe aplanarse u reorganizar grupos de rutas.
¿Funciona Expo Router con Nueva Arquitectura?
Sí en RN 0.86 y SDK 57 - las reglas de navegación no cambian. Prueba gestos y transiciones de pantalla en Android de nivel medio cuando habilitas Nueva Arquitectura.
¿Debo usar redirect en middleware?
Expo Router soporta exportaciones de redirección en archivos de ruta para casos simples. Las compuertas de autenticación con hidratación de sesión asincrónica todavía pertenecen en componentes de layout que esperan estado de autenticación antes de renderizar hijos.
¿Cómo pruebo navegación en Jest?
Usa renderRouter de @testing-library/react-native para pruebas de integración en stacks críticos. Prueba esquemas de parámetros unitariamente por separado. Reserva Maestro para flujos de inicio en frío de enlace profundo completo.
¿Qué rompe OTA vs compilaciones de tienda para enrutamiento?
Agregar rutas JS es seguro para OTA. Cambiar scheme, dominios asociados, o filtros de intención requiere una nueva compilación nativa y presentación en tienda.
¿Cómo manejo enlaces profundos pendientes después de login?
Almacena la URL inicial desde useURL() o configuración de enlace en memoria, completa login, luego router.replace a la ruta almacenada. Borra el valor pendiente después de la navegación para prevenir bucles.
Versiones de Stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (expo ~57.0.4).