Regras de Navegação e Roteamento
Convenções de roteamento baseadas em arquivos e higiene de deep-link para Expo Router no Expo SDK 57. Estas regras mantêm a navegação previsível à medida que a árvore app/ cresce, previnem bugs de autenticação e de parâmetros de URLs malformadas, e garantem que as builds de loja lidem corretamente com links universais no iOS e Android.
- Aplique o Nível 1 ao configurar
app/ ou adicionar um novo grupo de rotas - erros de layout são caros para desfazer.
- Revise os Níveis 2–3 em todo PR que toque em
app/, Link, router.push, ou configurações de esquema de app.config.
- Execute o Nível 4 antes da submissão para a loja - deep links e redirecionamentos de autenticação são as principais fontes de crash em análises de produção.
- Emparelhe regras de validação de parâmetros com testes de contrato Jest - lint não prova que um deep link malformado é rejeitado.
- Documente padrões de navegação não padronizados (modais como rotas vs overlays) em um ADR antes que a terceira equipe os copie.
-
Arquivos de rota reexportam telas de recursos: app/(tabs)/orders/index.tsx importa OrdersScreen de features/orders - sem hooks, chamadas de fetch ou Redux em arquivos de rota.
- Alvo: Arquivos de rota com menos de 20 linhas; a lógica vive em módulos de recursos testáveis.
- Rejeitar: Telas de 200 linhas commitadas diretamente em
app/.
-
Um único _layout.tsx raiz gerencia provedores globais: Tema, sessão de autenticação, cliente de consulta e limites de erro são montados uma vez em app/_layout.tsx - não repetidos em cada layout aninhado.
- Ordem: Splash → fontes → provedores →
<Slot /> ou stack - documente a sequência no README.
- Rejeitar:
QueryClientProvider duplicado em layouts de tab e stack.
-
Layouts aninhados lidam apenas com o chrome: _layout.tsx de Tab define ícones e barra de abas; _layout.tsx de Stack define headerShown e transições - não busca de dados de recursos.
- Padrão:
useFocusEffect refetch pertence ao componente de tela, não a _layout.tsx.
- Exceção: Layouts de portão de autenticação que redirecionam usuários não autenticados - mantenha a lógica de redirecionamento mínima.
-
Use grupos de rotas para organização, não comportamento em tempo de execução: (auth), (tabs) e (modals) agrupam arquivos sem afetar o caminho da URL - pastas com parênteses são para estrutura.
- Nomenclatura: Nomes de arquivos em kebab-case minúsculo correspondem a segmentos de URL -
order-detail.tsx → /order-detail.
- Rejeitar: Aninhamento profundo puramente para espelhar o organograma (
app/team-a/feature-b/...).
-
Rotas de índice para hubs de lista; segmentos dinâmicos para entidades: orders/index.tsx lista pedidos; orders/[id].tsx mostra um pedido - URLs previsíveis semelhantes a REST auxiliam deep linking e análises.
- Catch-all: Reserve
[...slug].tsx para suporte a CMS ou URLs legadas - valide segmentos imediatamente.
- Rejeitar: IDs opacos em query strings quando um segmento de caminho é mais claro (
/orders/123 em vez de /orders?id=123).
-
Coloque UI de carregamento e erro junto com as rotas ao usar limites Suspense: orders/[id].tsx pode exportar um orders/[id]/loading.tsx paralelo ou lidar com esqueletos na tela - não deixe flashes em branco durante a navegação.
- Padrão: A tela de recurso aceita a prop
isLoading; a rota configura o suspense se adotado.
- Testar:
renderRouter do RNTL para stacks críticos.
-
Prefira objetos href em vez de caminhos de string: router.push({ pathname: '/orders/[id]', params: { id } }) - rotas tipadas capturam erros de digitação em tempo de compilação quando experiments.typedRoutes está habilitado.
- Habilitar:
experiments: { typedRoutes: true } em app.config para projetos SDK 57.
- Rejeitar:
router.push('/orders/' + id) sem validação de parâmetros.
-
Use Link para navegação declarativa; router para imperativa: Link preserva a semântica de acessibilidade e prefetch - router.replace imperativo apenas após mutações ou redirecionamentos de autenticação.
- Pilha de volta:
router.replace após login/logout - push duplica telas de autenticação no gesto de voltar.
- Rejeitar:
router.push dentro de onPress quando Link com asChild e Pressable for suficiente.
-
Centralize constantes de rota em um único módulo: routes.ts exporta construtores de href - recursos importam orderDetailHref(id) em vez de espalhar strings de caminho.
- Monorepo: Compartilhe
packages/navigation para aplicativos white-label com formatos de rota idênticos.
- Rejeitar:
'/settings/account' copiado e colado em doze arquivos.
-
Modais e rotas de apresentação são explícitos: Modais de tela cheia recebem um grupo (modals) ou presentation: 'modal' nas opções de stack - overlays meio implementados confundem o comportamento de voltar no Android.
- Android: Teste o botão voltar do hardware em cada rota modal - ele deve fechar o modal, não sair do aplicativo.
- ADR: Documente quando usar
Modal do React Native vs um modal baseado em rota.
-
Proteja rotas autenticadas no layout, não com portões por tela: (app)/_layout.tsx verifica a sessão e redireciona para /(auth)/login - telas individuais não devem duplicar verificações de autenticação.
- Sessão obsoleta: Ouça a expiração do token no nível do layout e chame
router.replace.
- Rejeitar:
if (!user) return null em cada tela protegida sem redirecionamento.
-
Lide com a URL inicial e deep links de cold-start uma vez: useURL() ou a configuração de linking do Expo Router resolve a URL de inicialização - não analise Linking.getInitialURL() em cada tela.
- Padrão: O layout de autenticação lê o deep link pendente após o login e navega para o caminho armazenado.
- Testar: Fluxo Maestro abrindo
myapp://orders/42 de fora do aplicativo.
-
Declare scheme em app.config antes de enviar esquemas de URL personalizados: scheme: "acme" habilita links acme:// - deve corresponder à documentação de marketing e QA.
- Múltiplos esquemas: Use
scheme: ["acme", "acme-dev"] para variantes de ambiente - não um esquema para todas as builds.
- Verificar:
npx uri-scheme list após prebuild.
-
Configure domínios associados para links universais (iOS) e app links (Android): ios.associatedDomains e android.intentFilters em app.config - edição manual de arquivos nativos quebra no prebuild CNG.
- Plugin de configuração: Use o plugin
expo-router e plugins documentados de domínios associados.
- Rejeitar: Enviar links universais sem
apple-app-site-association hospedado e validado.
-
Valide parâmetros de rota na fronteira do recurso: Zod (ou similar) analisa useLocalSearchParams() antes de renderizar - deep links malformados mostram um erro amigável, não um redbox.
- Exemplo:
const { id } = orderParamsSchema.parse(params) em OrderDetailScreen.
- CI: Testes de contrato para esquemas de parâmetros - não E2E para cada combinação de parâmetro.
-
Nunca confie na entrada de query string para decisões de segurança: Deep-link ?admin=true não concede acesso de administrador - autorize no servidor e no estado da sessão.
- Sanitizar: Remova parâmetros desconhecidos; registre payloads rejeitados em staging.
- Rejeitar:
if (params.promo) aplicando descontos sem validação do servidor.
-
Documente a matriz de deep links suportados no README: Caminho, parâmetros necessários, requisito de autenticação e URL de exemplo por rota - suporte e marketing dependem desta tabela.
- Versão: Atualize a matriz quando as rotas mudarem de nome - links quebrados sobrevivem em campanhas de e-mail por anos.
- Ferramentas: Gere a matriz a partir de tipos de rota sempre que possível.
-
Rota de fallback para caminhos desconhecidos: [...unmatched].tsx ou +not-found.tsx mostra UI recuperável com um link para a página inicial - redboxes 404 padrão prejudicam as avaliações da loja.
- Análises: Registre caminhos não correspondentes para capturar campanhas quebradas.
- Testar: Abra
myapp://does-not-exist no Maestro smoke.
-
Carregue telas de tab pesadas de forma preguiçosa quando as abas são raramente visitadas: import() dinâmico para sub-telas de configurações ou ferramentas de administração - não para abas de receita principais que os usuários abrem a cada sessão.
- Medir: Analisador de bundle antes de dividir preguiçosamente - divisões prematuras adicionam latência.
- Rejeitar: Carregamento preguiçoso do caminho de primeira pintura da aba inicial.
-
Evite estado de navegação em singletons mutáveis globais: Passe parâmetros via roteador ou stores de recursos - let pendingOrderId no nível do módulo quebra em navegações rápidas e recargas OTA.
- Corrigir: Cache TanStack Query com chave pelo parâmetro de rota, ou parâmetros de rota como fonte de verdade.
- Cheiro:
global.pendingDeepLink definido em um arquivo, lido em outro.
-
Redefina a stack no logout: router.dismissAll() ou substitua para a stack de autenticação - o gesto de voltar não deve retornar a telas autenticadas com um token limpo.
-
Não bloqueie a primeira pintura no carregamento de fontes/ícones de navegação: Carregue ícones de abas de ativos estáticos; adie rotas de fontes personalizadas até que o layout raiz termine useFonts.
- Padrão: O layout raiz retorna
null até que as fontes carreguem - rotas filhas não duplicam portões de fontes.
- Rejeitar:
if (!fontsLoaded) return null no meio da ordem de hooks em uma tela (violação da regra de hooks).
-
Teste E2E de fumaça cobre três caminhos de deep-link: Inicialização, caminho protegido por autenticação e detalhe da entidade principal - Maestro YAML contra builds de prévia EAS, não apenas Expo Go.
- Fixar:
appId para o identificador do bundle de app.config.
- CI: Executar em candidatos a lançamento em dispositivos físicos.
-
Checklist de PR para mudanças de roteamento: Arquivo renomeado? Matriz de deep-link atualizada? Teste de esquema de parâmetro? Botão voltar do Android em modais? - anexe ao template de PR de roteamento.
- Mudança que quebra: Renomear uma rota é uma API que quebra links de marketing - versionar ou redirecionar caminhos antigos.
- Nota OTA: Adições de rotas apenas em JS são seguras para OTA; mudanças de esquema exigem uma build nativa.
- Nível 1 (1–6): Estrutura de arquivos e layout - estabeleça antes de adicionar recursos; mover telas depois quebra favoritos e análises.
- Nível 2 (7–12): API de navegação - previne caminhos tipados como string e lógica de autenticação duplicada.
- Nível 3 (13–18): Deep links - necessário antes que campanhas externas e links universais entrem em produção.
- Nível 4 (19–24): Desempenho e verificação - portões para candidatos a lançamento e promoções OTA.
A lógica de negócios deve viver em app/ ou features/?
Sempre em features/ - app/ é uma tabela de roteamento. Arquivos de rota importam e reexportam telas; eles não buscam dados nem possuem hooks além da fiação trivial.
Como habilito rotas tipadas no SDK 57?
Defina experiments: { typedRoutes: true } em app.config e use objetos href com router.push e Link. Execute npx expo customize tsconfig.json se o template ainda não o fez.
Onde os redirecionamentos de autenticação pertencem?
Em um layout dedicado ((app)/_layout.tsx ou (auth)/_layout.tsx) que verifica a sessão uma vez e chama router.replace. Evite redirecionamentos useEffect por tela que correm no cold start.
Esquema personalizado vs. links universais?
Esquemas personalizados (acme://) são mais fáceis de testar, mas podem ser sequestrados em algumas versões do Android. Links universais/app (https://app.acme.com/...) são necessários para campanhas de e-mail e web-para-app - configure ambos para aplicativos de produção.
Como valido parâmetros de deep-link?
Analise useLocalSearchParams() com Zod no topo da tela do recurso. Mostre um 404 ou estado de erro em caso de falha. Adicione testes Jest para o esquema - não Maestro para cada parâmetro inválido.
Modal como rota ou Modal do React Native?
Modais baseados em rota participam de deep linking e do botão voltar do Android - prefira-os para fluxos compartilháveis. Modal do RN serve para seletores efêmeros sem representação de URL. Documente a escolha em um ADR.
Posso renomear um arquivo de rota após o lançamento?
Sim em JS via OTA, mas URLs antigas quebram a menos que você adicione redirecionamentos em [...unmatched].tsx ou mantenha arquivos de alias. Trate caminhos de rota como uma API pública - versiona mudanças que quebram.
Quantos arquivos _layout.tsx são muitos?
Um raiz mais um por navegador principal (abas, stack de autenticação, grupo de modais) é típico. Mais de quatro layouts aninhados geralmente significa que a árvore deve ser achatada ou os grupos de rotas reorganizados.
Expo Router funciona com a Nova Arquitetura?
Sim no RN 0.86 e SDK 57 - as regras de navegação não mudam. Teste gestos e transições de tela em Android de gama média ao habilitar a Nova Arquitetura.
Devo usar redirect no middleware?
Expo Router suporta exportações de redirect em arquivos de rota para casos simples. Portões de autenticação com hidratação de sessão assíncrona ainda pertencem a componentes de layout que esperam pelo estado de autenticação antes de renderizar filhos.
Como testo a navegação em Jest?
Use @testing-library/react-native renderRouter para testes de integração em stacks críticos. Teste unitariamente esquemas de parâmetros separadamente. Reserve Maestro para fluxos completos de cold-start de deep-link.
O que quebra OTA vs. builds de loja para roteamento?
Adicionar rotas JS é seguro para OTA. Mudar scheme, domínios associados ou filtros de intenção requer uma nova build nativa e submissão para a loja.
Como lidar com deep links pendentes após o login?
Armazene a URL inicial de useURL() ou da configuração de linking na memória, complete o login e então router.replace para o caminho armazenado. Limpe o valor pendente após a navegação para evitar loops.
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).