PROGRAMA DE CURSO APOSTILA 03 · Navegação
← Início

Projeto Product com React Navigation

4 vídeos gravados pelo professor construindo, do zero, o mesmo projeto (my-app) usado no estudo de caso desta apostila. Clique em qualquer parte para assistir.

Introdução ao React Navigation

Diferente da web, o React Native não tem um navegador com histórico e URLs prontos. O React Navigation é a biblioteca padrão da comunidade para resolver isso: ela gerencia a pilha de telas, as transições, os parâmetros passados entre elas e a integração com os gestos e botões nativos (como o botão "voltar" do Android).

Os três tipos de navegador

📚

Stack Navigator

Empilha telas, uma sobre a outra — como páginas de um app. É o navegador mais usado e a base de qualquer fluxo.

🗂️

Tab Navigator

Abas fixas (geralmente na parte de baixo da tela) para alternar entre seções principais do app.

🧭

Drawer Navigator

Menu lateral deslizante, ótimo para apps com muitas opções de navegação secundárias.

💡 Eles se combinam
Na prática, um app real quase sempre combina os três: um Drawer que contém Tabs, e cada Tab contém sua própria Stack de telas. Vamos construir essa combinação passo a passo ao longo desta apostila.

Instalando as dependências

O React Navigation é modular: você instala um pacote core e, separadamente, o navegador que vai usar (Stack, Tab, Drawer...). Em um projeto Expo, rode:

terminal Shell
# Núcleo do React Navigation
npx expo install @react-navigation/native

# Dependências nativas exigidas pelo core
npx expo install react-native-screens react-native-safe-area-context

# Navegadores que vamos usar nesta apostila
npx expo install @react-navigation/native-stack
npx expo install @react-navigation/bottom-tabs
npx expo install @react-navigation/drawer
npx expo install react-native-gesture-handler react-native-reanimated
⚠️ Use sempre "expo install", não "npm install"
O comando npx expo install escolhe automaticamente a versão de cada pacote compatível com o SDK do seu projeto. Usar npm install pode instalar uma versão incompatível e quebrar o build nativo.

O NavigationContainer

Todo app que usa React Navigation precisa de um único <NavigationContainer> envolvendo toda a árvore de navegação. Ele gerencia o estado de navegação e a integração com o histórico do dispositivo.

App.js JavaScript
import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    <NavigationContainer>
      {/* Toda a navegação do app fica dentro daqui */}
    </NavigationContainer>
  );
}
@react-navigation/native react-native-screens react-native-safe-area-context Expo SDK 51+

Stack Navigator

O Stack Navigator empilha telas: ao navegar, a nova tela entra por cima da anterior (com uma animação de deslize), e o botão "voltar" desempilha. É o navegador que você vai usar para qualquer fluxo sequencial — login → cadastro, lista → detalhes, etc.

Criando a primeira Stack

App.js JavaScript
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import TelaHome from './screens/TelaHome';
import TelaDetalhes from './screens/TelaDetalhes';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        {/* Cada Stack.Screen é uma tela da pilha. "name" é o identificador da rota */}
        <Stack.Screen
          name="Home"
          component={TelaHome}
          options={{ title: 'Início' }}
        />
        <Stack.Screen
          name="Detalhes"
          component={TelaDetalhes}
          options={{ title: 'Detalhes' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Navegando entre telas

Toda tela registrada em um navegador recebe automaticamente a prop navigation, com métodos para navegar. Os mais usados são navigate, push e goBack.

screens/TelaHome.js JSX
import { View, Text, Button } from 'react-native';

export default function TelaHome({ navigation }) {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Tela Home</Text>

      {/* navigate: vai para a rota; se já existir na pilha, reaproveita */}
      <Button
        title="Ver detalhes"
        onPress={() => navigation.navigate('Detalhes')}
      />
    </View>
  );
}
MétodoO que faz
navigate('Rota')Vai para a rota. Se ela já está na pilha, volta para ela em vez de empilhar de novo.
push('Rota')Sempre empilha uma nova instância da tela, mesmo repetida.
goBack()Volta uma tela na pilha (mesmo efeito do botão nativo de voltar).
popToTop()Volta direto para a primeira tela da pilha.
replace('Rota')Troca a tela atual pela nova, sem manter a anterior na pilha.

Passando parâmetros entre telas

O segundo argumento de navigate é um objeto com os parâmetros da rota. A tela de destino lê esses valores através da prop route.params.

screens/TelaHome.js JSX
<Button
  title="Ver produto #42"
  onPress={() => navigation.navigate('Detalhes', {
    produtoId: 42,
    nome: 'Fone de ouvido Bluetooth',
  })}
/>
screens/TelaDetalhes.js JSX
import { View, Text } from 'react-native';

export default function TelaDetalhes({ route, navigation }) {
  // Os parâmetros enviados pelo navigate() chegam aqui
  const { produtoId, nome } = route.params;

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Produto #{produtoId}</Text>
      <Text>{nome}</Text>
    </View>
  );
}
💡 useRoute() e useNavigation()
Em componentes que não recebem navigation/route diretamente como prop (por exemplo, um componente filho dentro da tela), use os hooks useNavigation() e useRoute() do @react-navigation/native para acessá-los de qualquer lugar da árvore.

Customizando o cabeçalho (header)

App.js JavaScript
<Stack.Screen
  name="Detalhes"
  component={TelaDetalhes}
  options={{
    title: 'Detalhes do produto',   // Texto no header
    headerStyle: { backgroundColor: '#1a1a2e' },
    headerTintColor: '#fff',        // Cor do texto e da seta de voltar
    headerBackTitleVisible: false,  // Some com o texto ao lado da seta (iOS)
  }}
/>
⚠️ options também pode ser uma função
Quando o header precisa de dados dinâmicos (como o nome do produto no título), passe uma função em vez de um objeto: options={'{({ route }) => ({ title: route.params.nome })}'}.

Tab Navigator

O Tab Navigator organiza o app em seções principais acessíveis por abas — o padrão mais comum em apps mobile (Instagram, WhatsApp, iFood). Cada aba, por sua vez, costuma conter sua própria Stack de telas.

Criando as abas

App.js JavaScript
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import TelaInicio from './screens/TelaInicio';
import TelaBusca from './screens/TelaBusca';
import TelaPerfil from './screens/TelaPerfil';

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={{
          headerShown: false,        // Deixa o header por conta da Stack de cada aba
          tabBarActiveTintColor: '#00c48c',
          tabBarInactiveTintColor: '#999',
        }}
      >
        <Tab.Screen name="Início" component={TelaInicio} />
        <Tab.Screen name="Buscar" component={TelaBusca} />
        <Tab.Screen name="Perfil" component={TelaPerfil} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Adicionando ícones nas abas

O Expo já inclui a biblioteca @expo/vector-icons, com milhares de ícones prontos (Ionicons, MaterialIcons, FontAwesome...). A prop tabBarIcon recebe uma função que retorna o ícone, com a cor e o tamanho corretos para o estado ativo/inativo.

App.js JavaScript
import { Ionicons } from '@expo/vector-icons';

<Tab.Navigator
  screenOptions={({ route }) => ({
    headerShown: false,
    tabBarIcon: ({ color, size, focused }) => {
      const icones = {
        Início: focused ? 'home' : 'home-outline',
        Buscar: focused ? 'search' : 'search-outline',
        Perfil: focused ? 'person' : 'person-outline',
      };
      return <Ionicons name={icones[route.name]} size={size} color={color} />;
    },
  })}
>
  <Tab.Screen name="Início" component={TelaInicio} />
  <Tab.Screen name="Buscar" component={TelaBusca} />
  <Tab.Screen name="Perfil" component={TelaPerfil} />
</Tab.Navigator>
💡 Badge de notificação na aba
Use a prop tabBarBadge em um Tab.Screen específico (ex.: tabBarBadge: 3) para mostrar uma bolinha vermelha com contador — útil para mensagens não lidas ou itens no carrinho.

Drawer e Navegação Aninhada

O Drawer Navigator adiciona um menu lateral deslizante — útil quando o app tem opções secundárias (configurações, sobre, sair) que não merecem uma aba fixa. Na prática, ele costuma ficar "por fora" de tudo, envolvendo as Tabs.

Criando o Drawer

App.js JavaScript
import 'react-native-gesture-handler'; // Precisa ser o 1º import do arquivo!
import { NavigationContainer } from '@react-navigation/native';
import { createDrawerNavigator } from '@react-navigation/drawer';
import TelaConfiguracoes from './screens/TelaConfiguracoes';
import TelaSobre from './screens/TelaSobre';
import RotasPrincipais from './RotasPrincipais'; // as Tabs, extraídas para outro arquivo

const Drawer = createDrawerNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator screenOptions={{ headerShown: false }}>
        <Drawer.Screen name="Principal" component={RotasPrincipais} />
        <Drawer.Screen name="Configurações" component={TelaConfiguracoes} />
        <Drawer.Screen name="Sobre" component={TelaSobre} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}
⚠️ react-native-gesture-handler no topo do arquivo
O Drawer depende de gestos nativos. O import de react-native-gesture-handler precisa ser a primeira linha do arquivo de entrada do app (geralmente App.js ou index.js), antes de qualquer outro import.

Navegação aninhada: Drawer → Tabs → Stack

É assim que a maioria dos apps profissionais organiza a navegação: cada nível cuida de uma responsabilidade, e um navegador pode ficar dentro do outro sem limite de profundidade.

Drawer.Navigator ← menu lateral
  ├── Tab.Navigator     ← abas inferiores
  │   ├── Stack.Navigator (Início)  ← pilha de telas da aba
  │   │   ├── TelaInicio
  │   │   └── TelaDetalhes
  │   ├── Stack.Navigator (Buscar)
  │   └── Stack.Navigator (Perfil)
  ├── TelaConfiguracoes
  └── TelaSobre
RotasPrincipais.js JSX
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import TelaInicio from './screens/TelaInicio';
import TelaDetalhes from './screens/TelaDetalhes';
import TelaBusca from './screens/TelaBusca';
import TelaPerfil from './screens/TelaPerfil';

const Tab = createBottomTabNavigator();
const StackInicio = createNativeStackNavigator();

// Cada aba pode ter sua própria pilha de telas
function StackDaAbaInicio() {
  return (
    <StackInicio.Navigator>
      <StackInicio.Screen name="Início" component={TelaInicio} />
      <StackInicio.Screen name="Detalhes" component={TelaDetalhes} />
    </StackInicio.Navigator>
  );
}

export default function RotasPrincipais() {
  return (
    <Tab.Navigator screenOptions={{ headerShown: false }}>
      <Tab.Screen name="InícioTab" component={StackDaAbaInicio} options={{ title: 'Início' }} />
      <Tab.Screen name="Buscar" component={TelaBusca} />
      <Tab.Screen name="Perfil" component={TelaPerfil} />
    </Tab.Navigator>
  );
}

Abrindo e fechando o Drawer por código

screens/TelaInicio.js JSX
import { Button } from 'react-native';
import { useNavigation } from '@react-navigation/native';

export default function TelaInicio() {
  const navigation = useNavigation();

  return (
    <Button
      title="Abrir menu"
      onPress={() => navigation.openDrawer()}
    />
  );
}
💡 useNavigation() funciona em qualquer nível
Não importa quantos navegadores estejam aninhados: useNavigation() sempre devolve o navegador mais próximo do componente. Para acessar um navegador "pai" (como o Drawer a partir de uma tela dentro da Stack), o React Navigation propaga os métodos automaticamente — navigation.openDrawer() funciona mesmo de dentro da Stack.

Boas Práticas

Alguns cuidados evitam os erros mais comuns de quem está começando com navegação — e deixam o código mais fácil de manter conforme o app cresce.

1

Centralize os nomes das rotas

Evite strings soltas espalhadas pelo código. Um arquivo routes.js com constantes evita erros de digitação e facilita renomear rotas.

2

Tipifique os parâmetros (TypeScript)

Em projetos com TypeScript, declare um tipo RootStackParamList para que o editor avise quando um parâmetro obrigatório não foi passado.

3

Não abuse de telas aninhadas demais

Drawer → Tab → Stack já cobre a grande maioria dos apps. Cada nível extra de aninhamento aumenta a complexidade de manter o estado de navegação.

4

Use navigate por padrão, push só quando precisar repetir

navigate evita empilhar a mesma tela várias vezes sem querer, o que deixa o botão "voltar" com um comportamento estranho para o usuário.

Referência rápida

NavegadorPacoteQuando usar
Stack@react-navigation/native-stackFluxos sequenciais: lista → detalhes, login → cadastro.
Bottom Tabs@react-navigation/bottom-tabs3 a 5 seções principais do app, sempre acessíveis.
Drawer@react-navigation/drawerOpções secundárias: configurações, sobre, sair.
🚀 Próximos passos
Com Stack, Tab e Drawer dominados, o próximo passo natural é persistir dados entre telas e sessões — com AsyncStorage e SQLite — para que o app lembre do usuário e funcione offline. Esse é o tema da próxima apostila.

Projeto Guiado: Home → Product

Um projeto real, construído do zero em aula (my-app), com uma Stack simples entre duas telas: Home e Product. A seguir, o passo a passo de como ele foi montado — e, na sequência, como transformá-lo em um app com Bottom Tabs e Drawer, dois recursos que o projeto ainda não usa.

Visão geral da estrutura

my-app/
  ├── index.js            ← ponto de entrada (registra o App)
  └── src/
      ├── App.jsx          ← só renderiza <Routes />
      ├── routes/
      │   ├── index.jsx     ← NavigationContainer
      │   └── StackRoutes.jsx  ← Home → Product
      ├── screens/
      │   ├── Home.jsx
      │   └── Product.jsx
      └── components/
         ├── Header.jsx     ← linha com os itens (título + botão)
         ├── Title.jsx      ← texto de título padronizado
         └── ButtonIcon.jsx  ← botão de ícone reutilizável

Passo 1 — Criar o projeto e instalar o React Navigation

Os mesmos comandos da Seção 01, na ordem em que foram usados neste projeto:

terminal Shell
npx create-expo-app my-app --template blank
cd my-app

# Web (opcional, útil para testar rápido no navegador)
npx expo install react-dom react-native-web

# Ícones prontos (Material Icons e outros)
npx expo install @expo/vector-icons

# Núcleo do React Navigation
npm install @react-navigation/native

# Gerenciamento nativo de telas + respeita notch e barras do sistema
npx expo install react-native-screens react-native-safe-area-context

# Navegação em pilha (Stack)
npm install @react-navigation/native-stack
💡 Onde achar o nome dos ícones
O projeto usa @expo/vector-icons com a família MaterialIcons. Para descobrir o nome exato de cada ícone: icons.expo.fyi ou fonts.google.com/icons.

Passo 2 — Criar o contexto de navegação

routes/index.jsx é o único lugar do projeto com um NavigationContainer. Tudo que precisa navegar entra dentro dele.

src/routes/index.jsx JSX
// Arquivo responsável por criar um contexto de navegação.
import { NavigationContainer } from "@react-navigation/native";
import { StackRoutes } from "./StackRoutes";

export function Routes() {
  return (
    <NavigationContainer>
      <StackRoutes />
    </NavigationContainer>
  );
}

Passo 3 — Registrar as rotas em uma Stack

routes/StackRoutes.jsx declara as duas telas do app e esconde o header nativo (cada tela desenha o próprio cabeçalho com o componente Header).

src/routes/StackRoutes.jsx JSX
// Arquivo responsável por registrar e configurar as rotas do App.
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Home from "../screens/Home";
import Product from "../screens/Product";

const Stack = createNativeStackNavigator();

export function StackRoutes() {
  return (
    <Stack.Navigator initialRouteName="home" screenOptions={{ headerShown: false }}>
      <Stack.Screen name="home" component={Home} />
      <Stack.Screen name="product" component={Product} options={{ headerTitle: "Produtos" }} />
    </Stack.Navigator>
  );
}

Passo 4 — Componentes reutilizáveis e as telas

Três componentes pequenos formam o cabeçalho de cada tela: Header (a linha, em flexbox), Title (o texto do título) e ButtonIcon (um ícone tocável).

src/components/Header.jsx JSX
import { View } from "react-native";

export default function Header({ children }) {
  return (
    <View
      style={{
        width: '100%',
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginBottom: 54,
      }}>
      {children}
    </View>
  );
}
src/components/ButtonIcon.jsx JSX
import { TouchableOpacity } from "react-native";
import { MaterialIcons } from "@expo/vector-icons";

export default function ButtonIcon({ icon, ...rest }) {
  return (
    <TouchableOpacity {...rest}>
      <MaterialIcons name={icon} size={30} />
    </TouchableOpacity>
  );
}

Com os componentes prontos, as telas ficam curtas — cada uma monta seu próprio Header e usa useNavigation() para ir e voltar:

src/screens/Home.jsx JSX
import { View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import Header from '../components/Header';
import ButtonIcon from '../components/ButtonIcon';
import Title from '../components/Title';

export default function Home() {
  const navigation = useNavigation();

  return (
    <View style={{ flex: 1, padding: 32, paddingTop: 54 }}>
      <Header>
        <Title>Home</Title>
        <ButtonIcon icon="add-circle" onPress={() => navigation.navigate("product")} />
      </Header>
    </View>
  );
}
src/screens/Product.jsx JSX
import { View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import Header from '../components/Header';
import ButtonIcon from '../components/ButtonIcon';
import Title from '../components/Title';

export default function Product() {
  const navigation = useNavigation();

  return (
    <View style={{ flex: 1, padding: 32, paddingTop: 54 }}>
      <Header>
        <ButtonIcon icon="arrow-circle-left" onPress={() => navigation.goBack()} />
        <Title>Product</Title>
      </Header>
    </View>
  );
}
💡 O que já dá pra testar aqui
Com só esses arquivos o app já funciona: toque no ícone + na Home para empilhar a tela Product, e na seta para voltar. É a base sobre a qual os próximos dois passos entram.

Passo 5 — Ligando o Bottom Tabs ainda não usado no projeto

O pacote @react-navigation/bottom-tabs já está instalado no projeto e existe até um arquivo BottomRoutes.jsx — mas hoje ele só duplica a Stack, sem criar abas de verdade, e o routes/index.jsx continua usando a Stack diretamente. Veja como fechar essa parte: a aba "Início" recebe a Stack inteira (Home + Product), e uma segunda aba mostra uma tela nova.

terminal Shell
# já está no package.json deste projeto — rode só se estiver faltando
npx expo install @react-navigation/bottom-tabs
src/routes/BottomRoutes.jsx JSX
// Versão completa: cada aba mostra um conteúdo diferente, e a aba "Início"
// engloba a Stack inteira (Home → Product) em vez de uma tela só.
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { MaterialIcons } from "@expo/vector-icons";
import { StackRoutes } from "./StackRoutes";
import Perfil from "../screens/Perfil"; // tela nova, mesmo padrão de Home/Product

const Tab = createBottomTabNavigator();

export function BottomRoutes() {
  return (
    <Tab.Navigator screenOptions={{ headerShown: false }}>
      <Tab.Screen
        name="inicio"
        component={StackRoutes}
        options={{
          title: 'Início',
          tabBarIcon: ({ color, size }) => <MaterialIcons name="home" size={size} color={color} />,
        }}
      />
      <Tab.Screen
        name="perfil"
        component={Perfil}
        options={{
          title: 'Perfil',
          tabBarIcon: ({ color, size }) => <MaterialIcons name="person" size={size} color={color} />,
        }}
      />
    </Tab.Navigator>
  );
}

E trocar a Stack pela Tab como raiz da navegação:

src/routes/index.jsx JSX
import { NavigationContainer } from "@react-navigation/native";
import { BottomRoutes } from "./BottomRoutes"; // antes era StackRoutes

export function Routes() {
  return (
    <NavigationContainer>
      <BottomRoutes />
    </NavigationContainer>
  );
}

Passo 6 — Adicionando o Drawer não existe no projeto

O projeto ainda não tem nenhum arquivo de Drawer. Ele entra por fora de tudo: o Drawer vira a nova raiz, e o que hoje é a raiz (o Bottom Tabs) passa a ser só mais uma tela dele.

terminal Shell
npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
src/routes/DrawerRoutes.jsx JSX
// Arquivo novo: o Drawer engloba o Bottom Tabs e soma um item extra ao menu.
import { createDrawerNavigator } from "@react-navigation/drawer";
import { BottomRoutes } from "./BottomRoutes";
import Sobre from "../screens/Sobre"; // tela nova, só um texto de apresentação

const Drawer = createDrawerNavigator();

export function DrawerRoutes() {
  return (
    <Drawer.Navigator screenOptions={{ headerShown: false }}>
      <Drawer.Screen name="principal" component={BottomRoutes} options={{ title: 'Início' }} />
      <Drawer.Screen name="sobre" component={Sobre} options={{ title: 'Sobre' }} />
    </Drawer.Navigator>
  );
}
src/routes/index.jsx JSX
import { NavigationContainer } from "@react-navigation/native";
import { DrawerRoutes } from "./DrawerRoutes"; // antes era BottomRoutes

export function Routes() {
  return (
    <NavigationContainer>
      <DrawerRoutes />
    </NavigationContainer>
  );
}
⚠️ Este projeto usa index.js como entrada, não App.jsx
O import de react-native-gesture-handler precisa ser a primeira linha do arquivo de entrada real do projeto — aqui é index.js (raiz), não src/App.jsx:
index.js JavaScript
import 'react-native-gesture-handler'; // precisa vir antes de tudo
import { registerRootComponent } from 'expo';
import App from './src/App';

registerRootComponent(App);

Para abrir o Drawer a partir da Home, o mesmo ButtonIcon já usado no projeto resolve — só que agora chamando openDrawer():

src/screens/Home.jsx JSX
<Header>
  <ButtonIcon icon="menu" onPress={() => navigation.openDrawer()} />
  <Title>Home</Title>
  <ButtonIcon icon="add-circle" onPress={() => navigation.navigate("product")} />
</Header>

Estrutura final

my-app/
  ├── index.js            ← + import 'react-native-gesture-handler'
  └── src/
      ├── App.jsx
      ├── routes/
      │   ├── index.jsx      ← agora aponta pro DrawerRoutes
      │   ├── DrawerRoutes.jsx  ← novo
      │   ├── BottomRoutes.jsx  ← completo
      │   └── StackRoutes.jsx
      ├── screens/
      │   ├── Home.jsx       ← + botão de menu
      │   ├── Product.jsx
      │   ├── Perfil.jsx      ← novo (2ª aba)
      │   └── Sobre.jsx       ← novo (item do Drawer)
      └── components/
         ├── Header.jsx
         ├── Title.jsx
         └── ButtonIcon.jsx
🧭 Resultado
Drawer (menu lateral: Início / Sobre) → dentro dele, Bottom Tabs (Início / Perfil) → dentro da aba Início, a Stack original do projeto (Home → Product). Três navegadores, cada um resolvendo um problema diferente — exatamente o padrão ensinado na Seção 04.