ecosystem-wikicómo extender los templates sin romper sus convenciones
‹ todas las recipes

Rutas con expo-router

template-expo
último cambio 2026-09-25

El enrutado es por ficheros: cada .tsx de src/app/ es una ruta, y su ruta es su camino en disco. No hay fichero de rutas que mantener.

app/
  _layout.tsx          envoltorio de TODA la app
  index.tsx            /            bienvenida
  login.tsx            /login
  mfa-challenge.tsx    /mfa-challenge
  forgot-password.tsx  /forgot-password
  (home)/
    _layout.tsx        envoltorio del área privada
    index.tsx          /(home)      pantalla principal
    settings.tsx       /settings
    pet/[id].tsx       /pet/:id

El grupo entre paréntesis

(home) es un grupo: agrupa pantallas para darles un layout y unas reglas comunes sin aparecer en la URL. settings.tsx dentro de (home) se navega como /settings, no como /home/settings.

Aquí el grupo separa lo público de lo privado, y eso es toda la autorización de la app.

Proteger un área es tres líneas

En el _layout.tsx del grupo:

const { accessToken } = useAuth();
if (!accessToken) {
  return <Redirect href="/login" />;
}
return <Stack screenOptions={{ headerShown: false, animation: 'slide_from_right' }} />;

No hay guard por pantalla ni comprobación repetida: entrar en el grupo es la comprobación. Una pantalla nueva se protege con solo crearla dentro de (home).

El camino inverso también está cubierto: la pantalla de bienvenida redirige a /(home) si ya hay sesión, así que un usuario autenticado no ve el login.

Al añadir una pantalla, lo primero que hay que decidir es si va dentro o fuera de (home). Eso decide si requiere sesión.

Los dos layouts, y qué va en cada uno

app/_layout.tsx — envuelve todo. Aquí van los proveedores y la carga inicial: el AuthProvider, GestureHandlerRootView, las fuentes, el i18n y el control del splash. Se pinta null hasta que todo está listo.

app/(home)/_layout.tsx — solo el área privada. Aquí va lo que requiere sesión y debe ocurrir una sola vez: registrar el token de push y suscribirse a los toques en las notificaciones, los dos dependientes de accessToken.

Lo que necesita sesión va en el layout del grupo, no en el raíz. En el raíz se ejecutaría también para un usuario sin autenticar.

const router = useRouter();
router.push('/login');

// con parámetros, en forma de objeto — no concatenando cadenas
router.push({ pathname: '/pet/[id]', params: { id: petId, tab: 'historial' } });

Y al recibirlos, useLocalSearchParams(). Los parámetros llegan siempre como cadenas, así que un número o un booleano hay que convertirlos.

Con typedRoutes activado en app.json, una ruta mal escrita es un error de compilación y no un descubrimiento en ejecución.

Cuándo push y cuándo replace