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.
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:
# 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
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.
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>
{/* Toda a navegação do app fica dentro daqui */}
</NavigationContainer>
);
}
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
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.
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étodo | O 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.
<Button
title="Ver produto #42"
onPress={() => navigation.navigate('Detalhes', {
produtoId: 42,
nome: 'Fone de ouvido Bluetooth',
})}
/>
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>
);
}
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)
<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={'{({ 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
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.
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>
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
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
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.
├── Tab.Navigator ← abas inferiores
│ ├── Stack.Navigator (Início) ← pilha de telas da aba
│ │ ├── TelaInicio
│ │ └── TelaDetalhes
│ ├── Stack.Navigator (Buscar)
│ └── Stack.Navigator (Perfil)
├── TelaConfiguracoes
└── TelaSobre
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
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() 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.
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.
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.
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.
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
| Navegador | Pacote | Quando usar |
|---|---|---|
| Stack | @react-navigation/native-stack | Fluxos sequenciais: lista → detalhes, login → cadastro. |
| Bottom Tabs | @react-navigation/bottom-tabs | 3 a 5 seções principais do app, sempre acessíveis. |
| Drawer | @react-navigation/drawer | Opções secundárias: configurações, sobre, sair. |
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
├── 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:
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
@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.
// 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).
// 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).
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>
);
}
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:
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>
);
}
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>
);
}
+ 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.
# já está no package.json deste projeto — rode só se estiver faltando
npx expo install @react-navigation/bottom-tabs
// 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:
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.
npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
// 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>
);
}
import { NavigationContainer } from "@react-navigation/native";
import { DrawerRoutes } from "./DrawerRoutes"; // antes era BottomRoutes
export function Routes() {
return (
<NavigationContainer>
<DrawerRoutes />
</NavigationContainer>
);
}
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:
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():
<Header>
<ButtonIcon icon="menu" onPress={() => navigation.openDrawer()} />
<Title>Home</Title>
<ButtonIcon icon="add-circle" onPress={() => navigation.navigate("product")} />
</Header>
Estrutura final
├── 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