Listas e navegação no React Native com Expo Router
Use FlatList para renderizar dados e Expo Router para abrir uma rota dinâmica no React Native, com chaves, parâmetros e estados vazios.
FlatList renderiza listas de dados de forma virtualizada, e Expo Router liga
arquivos a endereços navegáveis. Nesta lição, uma lista de hábitos abre
/habitos/[id], mostra uma tela de detalhe e trata tanto a lista vazia quanto um
id que não existe.
Você pode partir do reducer criado na lição de estado e formulários. O foco agora é apresentar e localizar os dados, não mudar a regra de criação.
Uma vitrine móvel e um endereço para cada item
Imagine uma loja com milhares de produtos. A vitrine não coloca todo o estoque
na calçada; expõe o que está perto do olhar e troca as peças conforme você anda.
Esse é o mapa da virtualização: FlatList mantém uma janela de itens
renderizados em torno da área visível, em vez de montar toda a coleção de uma
vez.
Cada produto também tem um endereço no estoque. No Expo Router, o caminho
habitos/[id].tsx cria uma rota dinâmica: o trecho entre colchetes recebe um
valor, como /habitos/agua. A tela lê id e localiza o item correspondente.
O limite da vitrine é que virtualização não torna qualquer componente barato.
Se cada cartão fizer cálculos caros ou carregar imagens sem controle, a rolagem
ainda pode travar. Tecnicamente, FlatList reduz o número de views simultâneas,
mas seus renderItem, chaves e props continuam determinando atualizações.
Existe ainda uma diferença entre a coleção e sua apresentação. Ordenar,
filtrar ou agrupar deveria produzir os dados que a lista recebe; renderItem
deveria se concentrar num registro. Misturar busca, navegação e transformação
dentro de cada cartão repete trabalho conforme a janela virtualizada se move.
Prepare a coleção antes, dê identidade estável e passe callbacks claros.
Substitua o map por uma FlatList
Importe a lista e descreva o tipo de cada item:
import { FlatList, StyleSheet, Text } from 'react-native';
import { HabitCard } from '@/components/habit-card';
import type { Habit } from '@/types/habit';
type HabitListProps = {
habits: Habit[];
onToggle: (id: string) => void;
};Agora crie src/components/habit-list.tsx:
export function HabitList({ habits, onToggle }: HabitListProps) {
return (
<FlatList
contentContainerStyle={styles.content}
data={habits}
keyExtractor={(habit) => habit.id}
ListEmptyComponent={
<Text accessibilityLiveRegion="polite" style={styles.empty}>
Nenhum hábito ainda. Adicione o primeiro acima.
</Text>
}
renderItem={({ item }) => (
<HabitCard habit={item} onToggle={onToggle} />
)}
/>
);
}data é a coleção, renderItem diz como representar um item e
keyExtractor devolve uma identidade estável. Não use o índice como chave se a
lista pode inserir, remover ou reordenar itens: o índice muda de dono e o React
pode preservar estado visual no cartão errado.
FlatList funciona como um componente puro em vários caminhos: se um dado
externo usado por renderItem muda, mas data mantém a mesma referência, a
lista pode não saber que deve atualizar. Prefira incluir o valor nas props dos
itens ou informe extraData quando a renderização realmente depende dele. Não
use um objeto novo a cada renderização apenas para forçar tudo; isso esconde a
origem da atualização.
Complete o espaçamento:
const styles = StyleSheet.create({
content: { gap: 12, padding: 20, paddingBottom: 40 },
empty: { color: '#627d98', paddingVertical: 32, textAlign: 'center' },
});contentContainerStyle estiliza o contêiner interno, onde os itens ficam. A
prop style da própria lista atua no componente rolável externo. Essa diferença
explica por que centralizar o estado vazio apenas em style frequentemente não
move o conteúdo como esperado.
Faça o cartão navegar sem esconder sua ação principal
Um toque no checkbox alterna a conclusão; outro controle pode abrir detalhes. Separar as ações evita um cartão em que qualquer toque faz duas coisas. Adicione uma callback ao contrato:
type HabitCardProps = {
habit: Habit;
onOpen: (id: string) => void;
onToggle: (id: string) => void;
};Dentro do cartão, use uma área com nome explícito:
<Pressable
accessibilityLabel={`Abrir detalhes de ${habit.title}`}
accessibilityRole="button"
onPress={() => onOpen(habit.id)}
>
<Text style={styles.details}>Ver detalhes</Text>
</Pressable>Na lista, repasse onOpen; na tela inicial, obtenha o router:
import { useRouter } from 'expo-router';
const router = useRouter();
function openHabit(id: string) {
router.push({ pathname: '/habitos/[id]', params: { id } });
}
<HabitList habits={habits} onOpen={openHabit} onToggle={toggleHabit} />;push coloca uma nova entrada na pilha, portanto voltar retorna à lista. O
objeto com pathname e params reduz concatenação manual, mas o parâmetro de
URL ainda é texto. Não use esse id como autorização: uma rota identifica o que
a pessoa pediu para abrir; o servidor continua responsável por decidir o que
ela pode acessar.
Crie a rota dinâmica no lugar certo
No template atual do Expo SDK 57, as rotas ficam em src/app. Crie
src/app/habitos/[id].tsx:
import { Stack, useLocalSearchParams } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';
const knownHabits = {
agua: 'Beber água',
leitura: 'Ler por 20 minutos',
} as const;
export default function HabitDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const title = id ? knownHabits[id as keyof typeof knownHabits] : undefined;
if (!title) {
return <Text style={styles.message}>Hábito não encontrado.</Text>;
}
return (
<View style={styles.screen}>
<Stack.Screen options={{ title }} />
<Text style={styles.title}>{title}</Text>
<Text style={styles.message}>Continue um dia de cada vez.</Text>
</View>
);
}useLocalSearchParams lê parâmetros da rota focada. O tipo genérico ajuda o
TypeScript, mas não valida a entrada externa em tempo de execução. A busca no
objeto ainda precisa aceitar ausência e mostrar um estado coerente.
Acrescente os estilos no mesmo arquivo:
const styles = StyleSheet.create({
screen: { flex: 1, gap: 12, padding: 24 },
title: { color: '#102a43', fontSize: 28, fontWeight: '700' },
message: { color: '#486581', fontSize: 16, padding: 24 },
});Registre as telas na pilha
Configure títulos e o cabeçalho em src/app/_layout.tsx:
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Meus hábitos' }} />
<Stack.Screen name="habitos/[id]" options={{ title: 'Hábito' }} />
</Stack>
);
}O arquivo de rota define que a tela existe; Stack.Screen configura como ela
participa daquele layout. Você não precisa importar o componente de detalhe no
layout. Essa é a base da navegação orientada pelo sistema de arquivos, explicada
em profundidade na lição de Expo Router.
Reproduza uma chave instável e reconheça o risco
Este código compila, mas usa uma identidade frágil:
<FlatList
data={habits}
keyExtractor={(_, index) => String(index)}
renderItem={({ item }) => <HabitCard habit={item} />}
/>;Se um novo hábito entra no começo, o antigo índice 0 passa a representar
outro item. Não há necessariamente uma mensagem vermelha: é um erro de modelo
mental que aparece como foco, animação ou estado local associado ao cartão
errado. Corrija para keyExtractor={(habit) => habit.id} e garanta ids únicos.
A prova estrutural do projeto foi a exportação do router:
npx expo export --platform all --output-dir dist-proofO resultado real incluiu as duas rotas esperadas:
O bundle confirma que os módulos e caminhos foram resolvidos. A navegação por gesto, botão físico de voltar e leitor de tela ainda deve ser verificada num aparelho ou simulador antes de publicar.
Faça essa verificação em duas dimensões: histórico e restauração. Abra dois detalhes e use voltar para confirmar a ordem da Stack. Depois abra diretamente um endereço de detalhe na web ou por deep link e confirme que a tela não depende de uma variável deixada pela lista. Uma rota é uma entrada possível do aplicativo, não apenas o destino de um botão.
Missão: entregue uma lista que explique seus estados
Adicione três situações visíveis: vazia, com itens e id desconhecido. Depois ordene os pendentes antes dos concluídos sem alterar o array original.
Critérios de pronto:
- zero itens mostra uma orientação, não uma tela em branco;
- tocar em “Ver detalhes” abre /habitos/{id};
- /habitos/inexistente mostra “Hábito não encontrado”;
- a chave vem do id e não do índice;
- voltar preserva a lista anterior.Esse exercício liga identidade, virtualização e endereços. Na próxima aula, a gente busca dados pela rede e decide o que fica guardado no aparelho em APIs e armazenamento.
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, Expo SDK 57.0.15, Expo Router 57.0.15, React Native 0.86.2, React 19.2.3 e TypeScript 6.0.3, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- React Native — FlatList — reactnative.dev
- React Native — Optimizing FlatList Configuration — reactnative.dev
- Expo Router — Core concepts — docs.expo.dev
- Expo Router — Navigation — docs.expo.dev


