Dia 5 — Listas: FlatList e SectionList

Informatica · Conteudo · publicado em 05/10/2026
Dia 5 de 16

Listas: FlatList e SectionList

Aula 1

FlatList e o porquê de não usar map

FlatList e o porquê de não usar map

map num View desenha todos os itens, um por um, e o React Native monta

um componente nativo para cada um. Com trinta itens isso é invisível; com três

mil, são três mil caixas nativas criadas no mesmo quadro, a memória sobe e o app

começa a arrastar na rolagem. A causa não é o map: é que todos os itens

existem ao mesmo tempo.

FlatList resolve isso com virtualização: ela desenha só o que está

visível mais uma margem, e vai descartando o que saiu da tela. Do ponto de vista

do aluno, FlatList parece uma lista normal — o map sumiu de vista — mas por

trás existem duas propriedades que precisam estar presentes para a

virtualização funcionar:

PropriedadeO que faz
datao array que alimenta a lista
renderItema função que devolve o componente de cada item
keyExtractora função que dá a chave única de cada item

O exemplo desta página põe as duas formas lado a lado e o número é o mesmo: 40

componentes criados no map e 40 na lista. A contagem inicial é igual, e é

justamente esse número que engana: o que difere não é o que foi criado, é o que

ficou na memória depois. A pergunta certa não é "quantos componentes a lista

criou" e sim "quantos continuam vivos enquanto o usuário rola".

O exemplo responde à segunda pergunta com a conta da janela: item de altura 44

em tela de 700 dá 16 itens visíveis. Com map os 40 continuam na memória;

com lista sobram 17 — os 16 da tela mais um de margem. Os outros 23 foram

descartados e serão criados de novo quando voltarem. É esse descarte, e não

economia de tempo de criação, que mantém o app rolando liso.

keyExtractor não é decoração. Sem ele, o React Native cai no index da

posição, e a lista quebra no lugar errado quando um item é removido do começo:

a posição 3 deixa de ser a posição 3, o componente reaproveita o estado do item

errado, e o sintoma é um checkbox marcado no item vizinho. A chave tem que

viver no dado — o id do registro — e a função é só a forma de chegar nela.

<FlatList
  data={pedidos}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <ItemPedido pedido={item} />}
/>

O exemplo grava as duas funções lado a lado para a diferença ficar visível: com

o índice, o quarto pedido recebe a chave 3; com o id, recebe p4. São o

mesmo item e dois identificadores, e a chave por posição descreve onde o

item estava, enquanto a chave por id descreve qual item é. Quando um item

sai do começo da lista, a posição 3 passa a ser outro pedido — e o estado que o

React guardou para a chave 3 continua lá, no lugar errado.

renderItem recebe três coisas

O objeto que renderItem recebe tem item, index e separators. Usar

index como chave é justamente o erro acima; usá-lo para exibir "item 1 de 20"

também, porque em lista paginada o número muda conforme o carregamento. index

serve para o que realmente é posicional, que é raro.

A função é chamada uma vez por item visível, e é o lugar certo para a lógica

de apresentação do item: formatar a data, escolher o ícone pelo status, cortar

o texto. Deixar essa lógica dentro do renderItem e não no componente do item

significa que o componente do item continua testável e reaproveitável.

O item é o registro inteiro, com todos os campos — e é por isso que o

renderItem do exemplo desestrutura só o que precisa: ({ item }). Trazer o

index junto sem usar é a forma mais comum de deixar o dado disponível para o

uso errado mais tarde. separators é o terceiro membro do objeto: é o que

permite ao item saber que está no topo ou no fim de uma seção, e quem sabe

disso é o próprio item, não o renderItem.

A consequência de virtualização é que o renderItem roda mais de uma vez para o

mesmo item. Ele é criado, descartado ao sair da tela e criado de novo quando

volta. Nada pode ser guardado em variable de fora do componente do item, porque

o componente é destruído e recriado — quem guarda estado entre as duas vezes é o

id, que é a chave.

quando o ScrollView ainda serve

ScrollView com map dentro é aceitável quando a lista é curta e **não

cresce**: os dias da semana, as três opções de um seletor, os itens de um menu.

O critério é se o conteúdo vem de uma requisição: se a lista pode ter mil

itens, é FlatList desde o começo. Trocar depois dá trabalho, porque a

diferença não fica só na tag — muda a forma de carregar mais item, de

medir posição e de atualizar um item específico.

A propriedade que controla a margem é windowSize, e ela é o ajuste fino da

virtualização: ela diz quantas vezes a altura da tela vale a área montada. O

padrão renderiza algumas telas além do visível, o que dá rolagem mais estável ao

pular rápido. Baixar o valor economiza memória e aumenta o trabalho ao voltar

para cima, que é a troca real.

Para o comportamento de lista rolável dentro de coluna rolável, há um detalhe

conhecido: o FlatList em ScrollView com o mesmo eixo perde a virtualização,

porque o sistema não consegue saber a altura do conteúdo. A solução é

inverter: a lista na horizontal dentro de uma coluna vertical, que funciona

porque cada eixo tem a sua rolagem.

O que dá para reduzir na primeira montagem é initialNumToRender: quantos itens

são desenhados antes da lista ter a chance de medir. Valor baixo faz a abertura

mais rápida e empurra o resto para o primeiro quadro de rolagem; valor alto faz o

oposto. Nenhum dos dois valores conserta lista que cresce — os dois só ajustam

quando o custo aparece.

Exemplo

// `map` dentro de `View` cria um componente por item; e a lista com
// virtualizacao que desenha so o que esta visivel. O exemplo monta as
// duas arvores e compara quantos componentes cada uma produziu.
const { View, Text, StyleSheet } = require('react-native');

const estilos = StyleSheet.create({
  item: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#e6e8eb' },
  container: { flex: 1 },
});

// O array que alimenta a lista.
const pedidos = Array.from({ length: 40 }, (_, i) => ({
  id: 'p' + (i + 1),
  cliente: 'cliente ' + (i + 1),
  total: 10 + i * 3,
}));

// (1) o jeito que trava: `map` monta um componente para CADA item.
function ComMap() {
  return (
    <View style={estilos.container}>
      {pedidos.map((pedido) => (
        <Text key={pedido.id} style={estilos.item}>
          {pedido.cliente} - {pedido.total}
        </Text>
      ))}
    </View>
  );
}

// (2) o jeito da lista com virtualizacao: ela recebe `data` e chama
// `renderItem` so para o que cabe na tela. O exemplo desenha com `map`
// porque o componente de lista nao esta no ambiente de execucao; a
// logica do `renderItem` e a mesma.
function ComLista(props) {
  return (
    <View style={estilos.container}>
      {props.data.map((item) => props.renderItem({ item, index: -1 }))}
    </View>
  );
}

const arvoreMap = ComMap();
const filhosMap = arvoreMap.props.children;
console.log('com `map`, componentes criados:', filhosMap.length);
console.log('o ultimo item criado:', filhosMap[filhosMap.length - 1].props.children);

// `renderItem` e uma funcao: o componente do item e o que ela devolve.
const ItemPedido = ({ pedido }) => (
  <Text style={estilos.item}>{pedido.cliente}</Text>
);
const arvoreLista = ComLista({ data: pedidos, renderItem: ({ item }) => <ItemPedido pedido={item} /> });
console.log('com lista, componentes criados:', arvoreLista.props.children.length);
console.log('o mesmo total: a diferenca esta no que fica na memoria depois');

// A janela: e isso que a virtualizacao controla.
const alturaDoItem = 44;
const alturaDaTela = 700;
const janela = Math.ceil(alturaDaTela / alturaDoItem);
console.log('altura do item:', alturaDoItem);
console.log('itens visiveis:', janela);
console.log('com `map` existem', pedidos.length, 'na memoria; com lista,', janela + 1);

// `keyExtractor` sem `id` cai no indice, e o item reaproveita o estado errado.
function chavePorIndice(item, indice) {
  return String(indice);
}
function chavePorId(item) {
  return String(item.id);
}
console.log('chave por indice:', chavePorIndice(pedidos[3], 3), '| chave por id:', chavePorId(pedidos[3]));
console.log('o item da posicao 3 muda de id quando algo e removido do inicio');

Saída real

com `map`, componentes criados: 40
o ultimo item criado: [ 'cliente 40', ' - ', 127 ]
com lista, componentes criados: 40
o mesmo total: a diferenca esta no que fica na memoria depois
altura do item: 44
itens visiveis: 16
com `map` existem 40 na memoria; com lista, 17
chave por indice: 3 | chave por id: p4
o item da posicao 3 muda de id quando algo e removido do inicio
Aula 2

SectionList, itemSeparator e lista vazia

SectionList, itemSeparator e lista vazia

SectionList é o FlatList para dados agrupados: em vez de um array de

itens, recebe sections, e cada seção é um objeto com title, data e uma

key. É a estrutura que resolve lista de contatos por letra, menu por

categoria, e histórico de pedido por mês. O agrupamento já vem pronto na

propriedade data da seção — o componente não agrupa nada, ele exibe a

divisão que a lista recebeu.

O exemplo desta página monta as duas seções e imprime o que cada propriedade

entrega: duas seções no total, a de maio com 2 itens e a de junho com 1, e

as chaves como [ '2026-05', '2026-06' ]. A chave de cada seção é obrigatória

pelo mesmo motivo da chave de cada item — sem ela, o agrupamento volta a ser

identificado por posição, e remover um mês do topo reidentifica todos os outros.

A estrutura de cada seção é o que o renderSectionHeader recebe: o objeto

inteiro. O exemplo passa { section: secao } e o cabeçalho lê section.title,

section.data.length e section.total — três campos, e total só existe

porque a seção o carrega. Nenhum campo do cabeçalho é calculado pelo componente:

tudo o que ele mostra saiu do objeto.

<SectionList
  sections={secoes}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <Linha item={item} />}
  renderSectionHeader={({ section }) => <Titulo titulo={section.title} />}
/>

As duas propriedades que fazem a seção virar cabeçalho visual são

renderSectionHeader, que desenha o topo de cada seção, e

sectionStyle, que dá o estilo do container da seção. A primeira é função e

recebe { section } — o mesmo objeto de dados que veio em sections, então

dá para ler section.title, section.total ou qualquer campo que a seção

carregue.

o separador é componente, não estilo

itemSeparatorComponent não recebe um estilo: recebe um componente. É essa

a pegadinha, porque columnWrapperStyle e ListHeaderComponent recebem

estilo e componente em posicionamentos parecidos da documentação. A

consequência é que um separador que muda com a seção — linha em cima da

primeira, nenhuma depois da última — precisa de uma função que decide o que

devolver, e o highlighted que o separador recebe indica se está entre dois

itens ou no fim da seção.

O exemplo implementa exatamente essa função, e o par de console é a prova de

que a propriedade decide: com highlighted verdadeiro, o separador devolve um

View; com highlighted falso, devolve null e nada é desenhado. O mesmo

componente produz as duas coisas, e quem escolhe é a posição — não o estilo, e

não a seção.

A consequência prática é que separador que respeita a borda da seção se escreve

com uma condição dentro do componente, não com uma propriedade nova. null é o

"não desenha": não ocupa espaço, não aparece e não empurra o item seguinte.

ListEmptyComponent cobre o caso mais comum de erro percebido: a lista

carregou, veio vazia, e a tela mostra um retângulo branco sem explicação nenhuma.

O componente recebe a lista vazia como filho e devolve o que deve aparecer

nele — "nenhum registro ainda", com um botão de criar. Ele só aparece quando

data ou sections estão vazios, e é a diferença entre uma tela que parece

quebrada e uma tela que informa.

O exemplo testa os dois lados da condição, e o teste é feito com some sobre as

seções: com pelo menos uma seção com item, o resultado é true e o vazio não

aparece. Sem seções, a árvore devolvida não tem nenhum filho — 0 — que é

exatamente o estado em que o ListEmptyComponent assume. O componente de vazio

devolve Text com o texto "nenhum registro ainda", e o texto é lido em

props.children.

O erro de meio termo é o mais comum: mostrar o vazio e a lista ao mesmo

tempo. Isso acontece quando o estado inicial do aplicativo é "vazio" e o

carregamento ainda não começou — a tela mostra "nenhum registro" para quem

espera. A distinção que resolve são três estados, não dois: carregando, vazio

de verdade e com dados.

esticar o item em telas largas

columnWrapperStyle define a linha de colunas e é o que permite numColumns

maior que um: { flexDirection: 'row' } com flex: 1 no item distribui os

itens pela largura. numColumns é estático na lista, e mudar o valor depois

provoca aviso do sistema sobre a troca de coluna — o jeito certo é ler a

largura da janela e escolher o número antes de montar.

O exemplo mostra os dois estilos que fazem a grade funcionar e faz a conta da

altura: linha de colunas com { flexDirection: 'row' } e item com `{ flex:

1 }. Com flex: 1` no item, cada coluna divide a largura igualmente, e é por

isso que o columnWrapperStyle precisa vir com flexDirection: 'row' — sem ele,

a linha de colunas empilha os itens em vez de distribuí-los.

A conta fecha em Math.ceil de 4 por 2, que dá 2 linhas. O arredondamento

para cima é o detalhe: com 5 itens em 2 colunas, são 3 linhas e a última

fica com um item só. Uma grade que esconde a sobra deixa um item invisível, e

o sintoma é um item faltando sem nenhuma mensagem.

Exemplo

// A lista em secoes recebe `sections` (grupo com `title` e `data`), e o
// separador e um COMPONENTE, nao um estilo. O exemplo monta as secoes e
// mostra o que cada propriedade devolve.
const { View, Text, StyleSheet } = require('react-native');

const estilos = StyleSheet.create({
  cabecalho: { fontSize: 13, fontWeight: '700', color: '#57606a', padding: 8, backgroundColor: '#f6f8fa' },
  linha: { padding: 12, flexDirection: 'row', justifyContent: 'space-between' },
  separador: { height: 1, backgroundColor: '#e6e8eb' },
  vazio: { padding: 24, textAlign: 'center', color: '#8b949e' },
});

const secoes = [
  { key: '2026-05', title: 'maio de 2026', total: 2, data: [
    { id: 'p41', cliente: 'ana', total: 90 },
    { id: 'p42', cliente: 'bruno', total: 45 },
  ] },
  { key: '2026-06', title: 'junho de 2026', total: 1, data: [
    { id: 'p43', cliente: 'carla', total: 120 },
  ] },
];

// (1) `sections` e o agrupamento pronto; o componente nao agrupa nada.
function ListaEmSecoes(props) {
  return (
    <View>
      {props.sections.map((secao) => (
        <View key={secao.key}>
          <Text style={estilos.cabecalho}>{secao.title}</Text>
          {secao.data.map((item) => (
            <Text key={item.id} style={estilos.linha}>{item.cliente}</Text>
          ))}
        </View>
      ))}
    </View>
  );
}

const arvore = ListaEmSecoes({ sections: secoes });
console.log('quantas secoes:', arvore.props.children.length);

// (2) `renderSectionHeader` recebe o objeto da secao, inteiro.
for (const secao of secoes) {
  const cabecalho = { section: secao };
  console.log('cabecalho de', cabecalho.section.title, '| itens:', cabecalho.section.data.length, '| total:', cabecalho.section.total);
}

// (3) o total de itens e a soma do `data` de cada secao.
const total = secoes.reduce((soma, secao) => soma + secao.data.length, 0);
console.log('total de itens:', total);
console.log('chave de cada secao:', secoes.map((s) => s.key));

// (4) `itemSeparatorComponent` e um COMPONENTE: recebe `highlighted` e
// devolve o que vai ser desenhado entre dois itens.
function Separador(props) {
  return props.highlighted ? <View style={estilos.separador} /> : null;
}
console.log('separador entre dois itens:', Separador({ highlighted: true }).type);
console.log('separador no fim da secao:', Separador({ highlighted: false }));

// (5) `ListEmptyComponent` so aparece com a lista vazia.
function Vazio() {
  return <Text style={estilos.vazio}>nenhum registro ainda</Text>;
}
const temDados = secoes.some((secao) => secao.data.length > 0);
console.log('a lista tem dados:', temDados, '| o vazio aparece:', !temDados);
const arvoreVazia = ListaEmSecoes({ sections: [] });
console.log('sem secoes, a lista nao tem nenhum filho:', arvoreVazia.props.children.length);
console.log('o componente de vazio devolve:', Vazio().type, Vazio().props.children);

// (6) `columnWrapperStyle` e a linha das colunas, com `numColumns`.
const linhaDeColunas = StyleSheet.flatten({ flexDirection: 'row' });
const item = StyleSheet.flatten({ flex: 1 });
console.log('linha de colunas:', linhaDeColunas, '| item:', item);
console.log('4 itens em 2 colunas sao', Math.ceil(4 / 2), 'linhas');

Saída real

quantas secoes: 2
cabecalho de maio de 2026 | itens: 2 | total: 2
cabecalho de junho de 2026 | itens: 1 | total: 1
total de itens: 3
chave de cada secao: [ '2026-05', '2026-06' ]
separador entre dois itens: View
separador no fim da secao: null
a lista tem dados: true | o vazio aparece: false
sem secoes, a lista nao tem nenhum filho: 0
o componente de vazio devolve: Text nenhum registro ainda
linha de colunas: { flexDirection: 'row' } | item: { flex: 1 }
4 itens em 2 colunas sao 2 linhas