Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

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.

Rodolfo Mori5 min de leitura

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:

tsx
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:

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:

tsx
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:

tsx
type HabitCardProps = {
  habit: Habit;
  onOpen: (id: string) => void;
  onToggle: (id: string) => void;
};

Dentro do cartão, use uma área com nome explícito:

tsx
<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:

tsx
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:

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:

tsx
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:

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:

tsx
<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:

bash
npx expo export --platform all --output-dir dist-proof

O resultado real incluiu as duas rotas esperadas:

Static routes (4) ├── / └── /habitos/[id] Exported: dist-proof

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.

text
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.

  • flatlist react native
  • navegacao react native
  • expo router
  • rota dinamica
  • lista mobile

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. React Native — FlatList — reactnative.dev
  2. React Native — Optimizing FlatList Configuration — reactnative.dev
  3. Expo Router — Core concepts — docs.expo.dev
  4. Expo Router — Navigation — docs.expo.dev

Continue por aqui