Rotas Protegidas & Middleware
Portões de autenticação, verificações de função e prevenção de loops de redirecionamento. O Expo Router SDK 57 introduz Stack.Protected e Tabs.Protected com booleanos guard - o padrão recomendado em vez de espalhar <Redirect /> em todos os layouts. A hidratação da sessão ainda vive no contexto React suportado pelo armazenamento ../auth-session/ ; rotas protegidas reagem quando guard muda.
Cartão de receita de referência rápida - pronto para copiar e colar.
app/
├── _layout.tsx # SessionProvider + Stack.Protected
├── sign-in.tsx # guard={!session}
└── (app)/
├── _layout.tsx # stack autenticado
└── (tabs)/...
// app/_layout.tsx
import { Stack } from "expo-router" ;
import { SplashScreen } from "expo-router" ;
import { SessionProvider, useSession } from "@/features/auth" ;
SplashScreen. preventAutoHideAsync ();
export default function RootLayout () {
return (
< SessionProvider >
< SplashScreenController />
< RootNavigator />
</ SessionProvider >
);
}
function SplashScreenController () {
const { isLoading } = useSession ();
if ( ! isLoading) SplashScreen. hideAsync ();
return null ;
}
function RootNavigator () {
const { session } = useSession ();
return (
< Stack >
< Stack.Protected guard = { !! session}>
< Stack.Screen name = "(app)" />
</ Stack.Protected >
< Stack.Protected guard = { ! session}>
< Stack.Screen name = "sign-in" />
</ Stack.Protected >
</ Stack >
);
}
// app/sign-in.tsx
import { router } from "expo-router" ;
import { Button, View } from "react-native" ;
import { useSession } from "@/features/auth" ;
export default function SignInScreen () {
const { signIn } = useSession ();
return (
< View style = {{ flex: 1 , justifyContent: "center" , alignItems: "center" }}>
< Button
title = "Sign in"
onPress = { async () => {
await signIn ();
router. replace ( "/(app)/(tabs)" );
}}
/>
</ View >
);
}
Quando usar isso:
Autenticação binária - conjuntos de rotas para usuários logados vs. convidados
Portões de função - Stack.Protected aninhados para telas exclusivas de administradores
Visibilidade de abas - ocultar a aba profile até que o usuário esteja autenticado
Expiração de sessão - guard muda para false → usuário é redirecionado automaticamente de telas protegidas
Provedor de sessão, proteção de função aninhada, fallback de hub de redirecionamento e padrões à prova de loop.
// features/auth/model/SessionProvider.tsx
import { createContext, use, useEffect, useState, type PropsWithChildren } from "react" ;
import * as SecureStore from "expo-secure-store" ;
type Session = { userId : string ; role : "user" | "admin" } | null ;
const AuthContext = createContext <{
session : Session ;
isLoading : boolean ;
signIn : () => Promise < void >;
signOut : () => Promise < void >;
} | null >( null );
export function useSession () {
const ctx = use (AuthContext);
if ( ! ctx) throw new Error ( "useSession requires SessionProvider" );
return ctx;
}
export function SessionProvider ({ children } : PropsWithChildren ) {
const [ session , setSession ] = useState < Session >( null );
const [ isLoading , setLoading ] = useState ( true );
useEffect (() => {
SecureStore. getItemAsync ( "session" )
. then (( raw ) => setSession (raw ? JSON . parse (raw) : null ))
. finally (() => setLoading ( false ));
}, []);
const signIn = async () => {
const next : Session = { userId: "u1" , role: "user" };
await SecureStore. setItemAsync ( "session" , JSON . stringify (next));
setSession (next);
};
const signOut = async () => {
await SecureStore. deleteItemAsync ( "session" );
setSession ( null );
};
return (
< AuthContext value = {{ session, isLoading, signIn, signOut }}>
{children}
</ AuthContext >
);
}
// app/_layout.tsx - root guards
import { Stack } from "expo-router" ;
import { SessionProvider, useSession } from "@/features/auth" ;
function RootNavigator () {
const { session , isLoading } = useSession ();
if (isLoading) return null ;
const isLoggedIn = !! session;
const isAdmin = session?.role === "admin" ;
return (
< Stack >
< Stack.Protected guard = {isLoggedIn}>
< Stack.Protected guard = {isAdmin}>
< Stack.Screen name = "admin" />
</ Stack.Protected >
< Stack.Screen name = "(app)" />
</ Stack.Protected >
< Stack.Protected guard = { ! isLoggedIn}>
< Stack.Screen name = "sign-in" />
</ Stack.Protected >
</ Stack >
);
}
export default function RootLayout () {
return (
< SessionProvider >
< RootNavigator />
</ SessionProvider >
);
}
// app/index.tsx - optional redirect hub (avoid fighting Stack.Protected)
import { Redirect } from "expo-router" ;
import { useSession } from "@/features/auth" ;
export default function Index () {
const { session , isLoading } = useSession ();
if (isLoading) return null ;
if (session) return < Redirect href = "/(app)/(tabs)" />;
return < Redirect href = "/sign-in" />;
}
// app/(app)/(tabs)/_layout.tsx - protect individual tabs
import { Tabs } from "expo-router" ;
import { useSession } from "@/features/auth" ;
export default function TabsLayout () {
const { session } = useSession ();
const isLoggedIn = !! session;
return (
< Tabs >
< Tabs.Screen name = "home" options = {{ title: "Home" }} />
< Tabs.Protected guard = {isLoggedIn}>
< Tabs.Screen name = "inbox" options = {{ title: "Inbox" }} />
< Tabs.Screen name = "profile" options = {{ title: "Profile" }} />
</ Tabs.Protected >
</ Tabs >
);
}
Quando guard é false:
O usuário não pode navegar pelo cliente para telas protegidas (Link, router.push).
Se já estiver em uma tela protegida quando guard se tornar false, o roteador redireciona para a rota âncora (geralmente index) ou para a primeira tela disponível na pilha.
As entradas de histórico para essa subárvore protegida são removidas quando o guard muda para false.
Coloque a tela de login fora do grupo Stack.Protected autenticado para que os convidados sempre tenham um destino de fallback.
Anti-padrão Loop Correção index redireciona para /sign-in enquanto o sign-in redireciona para /Salto infinito Use Stack.Protected - apenas uma autoridade decide o fallback Ambos (auth) e (app) possuem index.tsx em / / ambíguoHub único app/index.tsx Redirecionamento antes de isLoading === false Flash de sign-in → home → sign-in return null até que a hidratação seja concluídasign-in dentro do grupo autenticadoSem fallback ao deslogar grupo guard={!session} para telas de autenticação router.push após o loginDe volta ao sign-in router.replace
// ❌ Risco de loop - dois layouts redirecionam em /
// (auth)/_layout.tsx
if ( ! session) return < Redirect href = "/sign-in" />;
// (app)/_layout.tsx
if ( ! session) return < Redirect href = "/sign-in" />;
// ✅ Fonte única de verdade
// app/_layout.tsx - apenas Stack.Protected
O Expo Router não envia middleware de rota no estilo Next.js em nativo. Na web, o roteamento é principalmente geração estática hoje - sem middleware de servidor personalizado para redirecionamentos de autenticação por solicitação.
Camada Nativo Web (SDK 57) Stack.Protected✅ Portão do cliente ✅ Portão do cliente <Redirect /> em layouts✅ ✅ Middleware de servidor ❌ ❌ (ainda não) Autenticação de rota de API Preocupação separada app/api/** + seu servidor
Trate as rotas protegidas como portões de UX , não como limites de segurança. Valide tokens em cada chamada de API - veja ../auth-session/mobile-auth-basics/mobile-auth-basics.md e ../auth-session/securestore-and-keychain-keystore/securestore-and-keychain-keystore.md .
Um usuário abrindo myapp://inbox/42 enquanto deslogado atinge uma tela protegida → o roteador redireciona para a primeira tela não protegida disponível (sign-in). Após o login, router.replace para o href pretendido:
const redirect = useGlobalSearchParams <{ redirect ?: string }>();
await signIn ();
router. replace ((redirect as `/${ string }` ) ?? "/(app)/(tabs)" );
Passe o parâmetro de consulta redirect ao enviar usuários para o sign-in.
Chamar signOut() muda guard para false. O histórico protegido é limpo automaticamente - o usuário deve pousar em sign-in sem router.replace manual se os layouts estiverem configurados corretamente.
Rotas protegidas ≠ segurança do servidor - URLs ainda estão no bundle. Correção: Autorização de API, expiração de token, fixação de certificado para aplicativos sensíveis.
Duplicar telas entre grupos Protected - O mesmo profile em dois grupos causa erro. Correção: Uma declaração; aninhar portões para funções.
Renderizar navegadores antes da hidratação - Breve guard falso causa cintilação de redirecionamento. Correção: SplashScreen + portão isLoading.
Usar disabled em abas nativas como autenticação - disabled apenas bloqueia toques na barra de abas; router.push ainda funciona. Correção: Tabs.Protected ou Stack.Protected raiz.
Exportação estática web de rotas protegidas - Telas protegidas são ignoradas no tempo de compilação; o acesso direto à URL ainda pode buscar JS. Correção: Não confie em SSG para sigilo.
Login modal sem âncora - Contexto de deep link perdido. Correção: unstable_settings.anchor - veja Rotas Modais e de Apresentação .
Alternativa Use Quando Não Use Quando Stack.ProtectedSDK 57+ shells de autenticação Você precisa suportar Expo Router < 5 <Redirect /> em layoutsHub simples, código legado Matriz de funções complexa (prefira Protected aninhado) Imperativo router.replace apenas Pós-login único Aplicação contínua de sessão Portões em nível de recurso nas telas Ação extra sensível (excluir conta) Substituir completamente a autenticação em nível de layout Sessão do servidor (rotas de API web) Atualização de token, RBAC no servidor Esperar que isso bloqueie a navegação nativa sozinho
Stack.Protected vs Redirect - qual vence?
Prefira Stack.Protected para acesso em nível de navegador no SDK 57+.
Use <Redirect /> em app/index.tsx como um hub de pouso, não em todos os layouts aninhados.
Não combine regras conflitantes na mesma rota.
Como os portões de função aninhados se compõem?
< Stack.Protected guard = {isLoggedIn}>
< Stack.Protected guard = {isAdmin}>
< Stack.Screen name = "admin" />
</ Stack.Protected >
< Stack.Screen name = "(app)" />
</ Stack.Protected >
/admin requer que ambos os portões sejam verdadeiros. /about sob o grupo externo requer apenas login.
O que acontece quando o guard muda enquanto o usuário está em uma tela protegida?
O roteador redireciona para a âncora ou a primeira tela disponível.
O histórico da tela protegida é removido da pilha.
Nenhum router.replace('/sign-in') manual é necessário se o sign-in for o fallback não protegido.
Posso usar expo-auth-session com rotas protegidas?
Como evito o loop sign-in ↔ home?
Espere por isLoading === false antes de qualquer redirecionamento.
Autoridade de portão única no Stack.Protected raiz.
router.replace após o login, nunca push.
Um único app/index.tsx - sem rotas de índice concorrentes nos grupos.
Existe middleware Expo Router como o Next.js?
Não em nativo - use portões do cliente.
Web - redirecionamentos de servidor estilo middleware ainda não são suportados; use Stack.Protected do cliente e estados de carregamento.
As rotas de API autenticam separadamente no servidor.
Versões do Stack: Esta página foi escrita para React 19.2.3 , React Native 0.86.0 e Expo SDK 57 (expo ~57.0.4).