Navegadores Aninhados
Stacks dentro de abas dentro de stacks sem perder o comportamento de voltar. Cada app/**/_layout.tsx declara um navegador; rotas filhas herdam esse shell. O Expo Router mapeia URLs para a tela aninhada correta - você navega com caminhos completos como /orders/42, não com os objetos { screen, params } do React Navigation.
Cartão de receita de referência rápida - pronto para copiar e colar.
app/
├── _layout.tsx # Stack Raiz
├── index.tsx
└── (app)/
├── _layout.tsx # Abas (ou NativeTabs)
└── (tabs)/
├── _layout.tsx # Barra de abas
├── home/
│ ├── _layout.tsx # Stack por aba
│ ├── index.tsx # /home
│ └── feed.tsx # /home/feed
└── settings/
├── _layout.tsx
└── index.tsx
// app/_layout.tsx - a stack raiz envolve o grupo de abas
import { Stack } from "expo-router" ;
export default function RootLayout () {
return (
< Stack screenOptions = {{ headerShown: false }}>
< Stack.Screen name = "(app)" />
< Stack.Screen
name = "compose"
options = {{ presentation: "modal" , title: "Nova postagem" }}
/>
</ Stack >
);
}
// app/(app)/(tabs)/_layout.tsx - as abas possuem quatro stacks
import { Tabs } from "expo-router" ;
export default function TabLayout () {
return (
< Tabs >
< Tabs.Screen name = "home" options = {{ title: "Home" }} />
< Tabs.Screen name = "search" options = {{ title: "Search" }} />
< Tabs.Screen name = "inbox" options = {{ title: "Inbox" }} />
< Tabs.Screen name = "profile" options = {{ title: "Profile" }} />
</ Tabs >
);
}
// app/(app)/(tabs)/home/_layout.tsx - stack dentro de uma aba
import { Stack } from "expo-router" ;
export default function HomeStackLayout () {
return (
< Stack >
< Stack.Screen name = "index" options = {{ title: "Home" }} />
< Stack.Screen name = "feed" options = {{ title: "Feed" }} />
< Stack.Screen name = "[id]" options = {{ title: "Post" }} />
</ Stack >
);
}
Quando usar isso:
Abas + pushes de detalhes - cada aba mantém sua própria pilha de volta
Modal sobre abas - a stack raiz apresenta compose acima do shell da aba
Grupo de autenticação dentro da stack raiz - (auth) e (app) como stacks irmãs
Abas nativas + cabeçalhos - aninhe <Stack /> dentro de cada pasta de aba nativa
Stack raiz → abas → stacks por aba, com comportamento de volta previsível e destinos de link profundo.
// app/_layout.tsx
import { Stack } from "expo-router" ;
import { SessionProvider } from "@/features/auth" ;
export default function RootLayout () {
return (
< SessionProvider >
< Stack >
< Stack.Screen name = "(app)" options = {{ headerShown: false }} />
< Stack.Screen name = "(auth)" options = {{ headerShown: false }} />
</ Stack >
</ SessionProvider >
);
}
// app/(app)/(tabs)/_layout.tsx
import { Tabs } from "expo-router" ;
export default function TabsLayout () {
return (
< Tabs screenOptions = {{ headerShown: false }}>
< Tabs.Screen name = "home" options = {{ tabBarLabel: "Home" }} />
< Tabs.Screen name = "inbox" options = {{ tabBarLabel: "Inbox" }} />
</ Tabs >
);
}
// app/(app)/(tabs)/inbox/_layout.tsx
import { Stack } from "expo-router" ;
export default function InboxStackLayout () {
return (
< Stack >
< Stack.Screen name = "index" options = {{ title: "Inbox" }} />
< Stack.Screen name = "[threadId]" options = {{ title: "Thread" }} />
</ Stack >
);
}
// app/(app)/(tabs)/inbox/index.tsx
import { Link, router } from "expo-router" ;
import { FlatList, Pressable, Text } from "react-native" ;
const threads = [{ id: "t1" , subject: "Hello" }, { id: "t2" , subject: "Ship it" }];
export default function InboxListScreen () {
return (
< FlatList
data = {threads}
keyExtractor = {( item ) => item.id}
renderItem = {({ item }) => (
< Pressable onPress = {() => router. push ( `/inbox/${ item . id }` )}>
< Text style = {{ padding: 16 }}>{item.subject}</ Text >
</ Pressable >
)}
/>
);
}
// app/(app)/(tabs)/inbox/[threadId].tsx
import { Link, useLocalSearchParams, useRouter } from "expo-router" ;
import { Button, Text, View } from "react-native" ;
export default function ThreadScreen () {
const { threadId } = useLocalSearchParams <{ threadId : string }>();
const router = useRouter ();
return (
< View style = {{ flex: 1 , padding: 16 }}>
< Text >Thread: {threadId}</ Text >
< Button title = "Back" onPress = {() => router. back ()} />
< Link href = "/compose" asChild >
< Button title = "Reply (modal)" />
</ Link >
</ View >
);
}
// Deep link - lands on thread inside inbox tab, back returns to inbox list
// myapp://inbox/t2
import { router } from "expo-router" ;
router. push ( "/inbox/t2" );
O React Navigation aninha navegadores; o Expo Router expressa a mesma árvore através de pastas:
Usuário em /inbox/t2 pressiona voltar no Android:
1. A Stack do Inbox desempilha t2 → /inbox (lista)
2. Usuário pressiona voltar novamente → permanece na aba Inbox (a raiz da aba não tem para onde desempilhar)
3. Usuário muda para a aba Home → o estado da stack do Inbox é preservado (padrão)
4. Usuário abre o modal /compose → voltar fecha o modal, as abas permanecem por baixo
Controle o comportamento de reset da aba com unmountOnBlur em Tabs.Screen quando stacks obsoletas são inaceitáveis (por exemplo, fluxos de assistente).
Ao usar Abas Nativas , cada pasta de aba recebe seu próprio _layout.tsx exportando <Stack />:
// app/(app)/(native-tabs)/inbox/_layout.tsx
import { Stack } from "expo-router" ;
export default function InboxStack () {
return < Stack />;
}
Os gatilhos de aba nativa não desempilham automaticamente stacks aninhadas, a menos que disablePopToTop seja false (padrão no Android SDK 55+).
Fazer link profundo diretamente para uma rota modal sem uma âncora apaga a tela por trás dela. Exporte unstable_settings na stack que possui o modal:
// app/(app)/_layout.tsx
export const unstable_settings = {
anchor: "(tabs)" ,
};
import { Stack } from "expo-router" ;
export default function AppLayout () {
return (
< Stack >
< Stack.Screen name = "(tabs)" />
< Stack.Screen name = "filters" options = {{ presentation: "modal" }} />
</ Stack >
);
}
Objetivo Expo Router Abrir thread na aba de inbox router.push("/inbox/t2")Mudar de aba e resetar sua stack router.replace("/home")Abrir modal acima das abas router.push("/compose")Voltar para a raiz da aba router.dismissTo("/inbox") ou re-tocar na aba (abas nativas)
Prefira router.replace após transições de autenticação para que os usuários não possam voltar para telas de credenciais - veja ../expo-router/redirects-and-index-routes/redirects-and-index-routes.md .
Esperando uma única pilha de volta global - Cada aba mantém sua própria pilha. Correção: Projete URLs por aba; documente quais fluxos usam replace vs push.
Link profundo de modal sem âncora - Abrir /compose friamente sem contexto de aba. Correção: Defina unstable_settings.anchor na stack pai.
Duplicando telas entre navegadores - O mesmo profile.tsx em duas pastas de aba causa rotas ambíguas. Correção: Um arquivo por URL; use rotas compartilhadas se realmente necessário.
Retornar null do layout raiz - Bloqueia a renderização estática e pode quebrar a hidratação aninhada. Correção: Tela de splash + provedor de sessão; renderize <Stack /> quando estiver pronto.
Push com caminhos relativos entre abas - ../settings de /inbox/t2 pode não chegar onde você espera. Correção: Use hrefs absolutos (/settings) com rotas tipadas.
Aninhar Drawer dentro de Tabs sem planejamento - Conflitos de gestos e cabeçalhos duplos. Correção: Drawer na raiz ou abas dentro do drawer - escolha um padrão principal por aplicativo.
Alternativa Use Quando Não Use Quando Stack raiz única (sem abas) Fluxos lineares, assistentes de onboarding Quatro ou mais destinos de nível superior Abas sem stacks por aba Conteúdo de aba plano, sem pushes de detalhes Cada aba precisa de lista → detalhe Abas nativas + stacks por aba Barra de abas da plataforma + cabeçalhos Você precisa de UI de aba totalmente personalizada Uma stack + UI baseada em segmentos Layout simples de duas painéis para tablet Pilhas de volta verdadeiramente independentes por seção Componente Modal do React Native Alertas efêmeros, sem necessidade de URL O fluxo deve ser linkável profundamente ou estar no histórico
Por que o botão voltar do Android permanece na aba em vez de sair do aplicativo?
O botão voltar desempilha o navegador mais interno primeiro.
Na tela raiz de uma aba, não há mais nada para desempilhar dentro dessa aba.
Use BackHandler em pontos de saída reais do aplicativo (por exemplo, na raiz da aba inicial) se o produto exigir duplo-back-to-exit.
Como resetar a stack de uma aba ao re-selecioná-la?
Abas JavaScript: configure os listeners de pressionamento de aba através das opções do React Navigation.
Abas nativas: o re-tocar padrão desempilha para a raiz; use disablePopToTop em NativeTabs.Trigger para manter a profundidade.
Posso aninhar um Drawer dentro de Tabs?
app/(app)/_layout.tsx → Drawer
app/(app)/(drawer)/_layout.tsx → Stack ou filho Tabs
Sim - mas prefira um shell principal. Aninhamento profundo aumenta a confusão da pilha de volta e o tamanho do bundle.
Como funcionam as rotas tipadas com aninhamento?
Modais devem ficar na stack raiz ou dentro de uma stack de aba?
Stack raiz - composição global, filtros, seletor de mídia (visível de qualquer aba).
Stack de aba - ações de detalhes com escopo para uma seção (editar um único tópico de inbox).
Modais raiz precisam de anchor para links profundos.
Como testar navegação aninhada?
import { renderRouter, screen } from "expo-router/testing-library" ;
renderRouter ({
"inbox/index" : () => < InboxListScreen />,
"inbox/[threadId]" : () => < ThreadScreen />,
});
Afirme segmentos de pathname após router.push - veja ../testing/react-native-testing-library/react-native-testing-library.md .
Versões de Stack: Esta página foi escrita para React 19.2.3 , React Native 0.86.0 , e Expo SDK 57 (expo ~57.0.4).