Dia 11 — A consulta dentro do app: um hook de dados
Buscar no banco e colocar na tela
Buscar no banco e colocar na tela
Consulta que roda dentro do onPress de um botão é consulta que alguém esqueceu de chamar. O lugar certo é o useEffect, que roda quando o componente monta, e a tela precisa de três estados: carregando, com dado e vazio.
const [tarefas, setTarefas] = useState([]); const [carregando, setCarregando] = useState(true); const [erro, setErro] = useState(null); useEffect(() => { let cancelado = false; async function carregar() { try { setCarregando(true); const [resultado] = await banco.executeSql('SELECT * FROM tarefas ORDER BY prazo'); if (!cancelado) setTarefas(resultado.rows); } catch (falha) { if (!cancelado) setErro(falha.message); } finally { if (!cancelado) setCarregando(false); } } carregar(); return () => { cancelado = true; }; }, []);
É consultar no banco dentro do ciclo de vida do componente, e não no clique: o useEffect roda na montagem, e é ele quem garante que abrir a tela já mostre o dado. A consulta que depende de botão é a que falta quando o usuário volta de outra tela, recarrega o aplicativo ou abre pelo link.
O nome em inglês do estado de carregamento é loading, e ele é o primeiro dos três estados da tela. O estado da consulta em si é o setState do array: a consulta devolve as linhas, e quem guarda é o setTarefas.
A variável cancelado é a limpeza do efeito: quando o componente desmonta antes de a consulta responder, o setTarefas não deve rodar, porque ele tentaria atualizar estado de um componente que não existe mais.
Sem essa variável, o usuário navega para outra tela antes de a consulta voltar, e o setTarefas roda em um componente desmontado — o aviso sai no console e a tela nova fica com o dado da tela antiga. É o primeiro caso de erro de consulta que a tela precisa tratar, e é o único que o try e o catch não pegam.
Os três estados da tela
A tela precisa saber em qual dos três ela está, e o erro mais comum do curso é saltar direto para o primeiro e esquecer o terceiro.
{carregando && <Text>Carregando...</Text>} {erro && <Text>Não foi possível ler o banco.</Text>} {!carregando && !erro && tarefas.length === 0 && <Text>Nenhuma tarefa ainda.</Text>} {tarefas.map((tarefa) => ( <Text key={tarefa.id}>{tarefa.titulo}</Text> ))}
A ordem das condições importa: carregando vem primeiro, erro depois, e o resultado vazio vem por último, protegido pelos dois. Invertendo, o estado vazio aparece junto com o "Carregando..." e a tela mostra duas mensagens ao mesmo tempo.
O map no fim é o que renderizar resultado — o array do estado virando Text, um por linha, com a chave que o id do banco fornece. Esse é o ponto onde a consulta deixa de ser consulta e vira tela.
carregando precisa virar false mesmo quando a consulta falhou, e é por isso que o finally existe. Sem ele, a tela fica em "Carregando..." para sempre depois de um erro — o aplicativo parece travado em vez de ter falhado. O exemplo desta página mostra o antes e o depois: o estado sem o finally continua com carregando verdadeiro mesmo depois da falha.
O array de dependências
O [] no fim do efeito diz que ele roda uma vez, na montagem. Uma consulta depende de outras coisas — o filtro, o texto da busca, o id da edição — e essas coisas entram no array:
useEffect(() => { carregarTarefas(filtro); }, [filtro]);
É a dependência do efeito que decide quando a consulta refaz, e ela é a parte do useEffect que mais gera bug. Dependência faltando é o primeiro caso: filtro que muda e a lista não acompanha. Dependência desnecessária é o outro: colocar um objeto que muda a cada renderização dentro do array faz o efeito rodar infinitamente.
O [] do começo e o [] do fim não são o mesmo []. O do começo é o estado inicial do useState, que roda uma vez; o do fim é a lista de dependências, que diz quando o efeito repete. Escrever useState([]) e useEffect(fn, []) com o mesmo array não significa a mesma coisa, e é essa confusão que faz o aluno achar que o efeito roda a cada tecla.
A regra prática é: a dependência do efeito é a lista do que o efeito lê de fora. Se o corpo usa filtro, filtro entra no array. Se o corpo usa uma função que muda de identidade a cada desenho, essa função entra também — e é por isso que a função costuma vir de um useCallback, para poder entrar no array sem refazer o efeito.
Exemplo
// A consulta entra no ciclo de vida do componente: roda uma vez na // montagem, e a tela tem tres estados — carregando, com dado e vazio. const { View, Text } = require('react-native'); const { useState, useEffect } = require('react-native'); const banco = { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }, { id: 2, titulo: 'Ler o capitulo 4', prazo: '2026-09-12', feita: 1 }, ] }; // o corpo do componente: os tres estados e o efeito function TelaDeTarefas() { const [tarefas, setTarefas] = useState([]); const [carregando, setCarregando] = useState(true); const [erro, setErro] = useState(null); useEffect(() => { let cancelado = false; async function carregar() { try { setCarregando(true); const [resultado] = await banco.executeSql('SELECT * FROM tarefas ORDER BY prazo'); if (!cancelado) setTarefas(resultado.rows); } catch (falha) { if (!cancelado) setErro(falha.message); } finally { if (!cancelado) setCarregando(false); } } carregar(); return () => { cancelado = true; }; }, []); return ( <View> {carregando ? <Text>Carregando...</Text> : null} {erro ? <Text>Não foi possível ler o banco.</Text> : null} {!carregando && !erro && tarefas.length === 0 ? <Text>Nenhuma tarefa ainda.</Text> : null} {tarefas.map((tarefa) => ( <Text key={tarefa.id}>{tarefa.titulo}</Text> ))} </View> ); } // o mock de `useEffect` roda o efeito na hora; o exemplo mostra o que a // arvore da tela montou const arvore = TelaDeTarefas(); console.log('elemento devolvido:', arvore.type); const filhos = arvore.props.children; console.log('filhos da tela:', Array.isArray(filhos) ? filhos.length : 1); // os tres estados, escritos a mao porque o `set` do mock nao muda nada function montar(carregando, erro, tarefas) { const partes = []; if (carregando) partes.push('Carregando...'); if (erro) partes.push('Não foi possível ler o banco.'); if (!carregando && !erro && tarefas.length === 0) partes.push('Nenhuma tarefa ainda.'); for (const tarefa of tarefas) partes.push(tarefa.titulo); return partes; } console.log('carregando:', montar(true, null, [])); console.log('com dado: ', montar(false, null, banco.tarefas)); console.log('vazio: ', montar(false, null, [])); console.log('com erro: ', montar(false, 'no such table: tarefas', [])); // sem o `finally`, o estado de carregando nunca vira false depois do erro function semFinally(falhou) { const estados = { carregando: true }; if (!falhou) estados.carregando = false; return estados; } console.log('sem o finally, depois da falha:', JSON.stringify(semFinally(true)));
Saída real
elemento devolvido: View
filhos da tela: 4
carregando: [ 'Carregando...' ]
com dado: [ 'Revisar o WHERE', 'Ler o capitulo 4' ]
vazio: [ 'Nenhuma tarefa ainda.' ]
com erro: [ 'Não foi possível ler o banco.' ]
sem o finally, depois da falha: {"carregando":true}
O hook que carrega, recarrega e salva
O hook que carrega, recarrega e salva
Consultar, gravar e recarregar é o mesmo trio em toda tela do aplicativo. Quando ele fica escrito dentro do componente, cada tela repete a mesma estrutura e cada repetição é um lugar onde o catch pode faltar. A solução é um hook próprio, com o nome da consulta dentro dele.
import { useCallback, useEffect, useState } from 'react'; function useTarefas(filtro = 'todas') { const [tarefas, setTarefas] = useState([]); const [carregando, setCarregando] = useState(true); const [erro, setErro] = useState(null); const recarregar = useCallback(async () => { try { setCarregando(true); setErro(null); const linhas = await buscarDoBanco(filtro); setTarefas(linhas); } catch (falha) { setErro(falha.message); } finally { setCarregando(false); } }, [filtro]); useEffect(() => { recarregar(); }, [recarregar]); return { tarefas, carregando, erro, recarregar }; }
É o hook de dados do trimestre, e ele é um hook customizado como qualquer outro: uma função que começa com use e usa hooks por dentro. O nome do hook reutilizável é o nome da consulta — useTarefas, useCategorias, useConfiguracao — e ele vira a forma de o aplicativo falar sobre dados em vez de repetir a mesma sequência.
O componente vira curto, e a parte que importa é a assinatura: recarregar volta junto com os dados. É o refetch do React Query, escrito à mão, e a biblioteca que implementa esse desenho chama de useQuery.
O detalhe que amarra o desenho é o useCallback: sem ele, recarregar seria uma função nova a cada desenho, e o useEffect que depende dela rodaria a cada desenho — ou seja, a consulta ao banco dispararia sem parar. Com o useCallback e o [filtro], a função só muda quando o filtro muda, e o efeito só refaz a consulta quando o filtro muda.
O exemplo desta página imprime o que o hook devolve — as quatro chaves, tarefas, carregando, erro e recarregar — e mostra que o filtro muda o resultado da consulta. É o contrato do hook: quem chama não sabe de onde veio o dado, e só precisa saber o que recebe.
Por que ler de novo depois de gravar
Depois de um INSERT bem-sucedido, a tela tem duas opções: acrescentar a linha no estado, ou reler a lista. A segunda parece desperdício e é a correta — porque o banco pode ter mudado de maneiras que o aplicativo não sabe: um DEFAULT, um gatilho, uma linha que outra tela gravou. A tela que mantém cópia própria do banco diverge da primeira vez que o usuário puxa para baixo.
É salvar e recarregar como um par inseparável, e a ordem importa: gravar primeiro, reler depois. Reler antes de gravar devolve a lista sem a linha nova, e a tela mostra um estado que nunca existiu.
O mesmo vale para DELETE: recarregar depois de apagar não custa, e garante que o que sumiu da tela sumiu da verdade também. É atualizar lista como consequência da gravação, e não como um passo que alguém precisa lembrar de escrever.
Separar leitura de escrita
Um hook que faz as duas coisas vira um hook que ninguém sabe quando chamar. A divisão que funciona é o separar leitura de escrita: o hook devolve a leitura e a escrita separadas, cada uma com o seu nome.
const { tarefas, carregando, recarregar } = useTarefas(filtro); const { criar, apagar, atualizando } = useEscritaTarefas(recarregar);
A divisão dá um estado único para a lista: uma cópia só, que a leitura preenche e a escrita invalida. Com duas cópias — uma que a tela desenha e outra que a escrita atualiza — qualquer caminho que esqueça de copiar uma para a outra divergem, e a divergência aparece como item faltando ou duplicado.
recarregar entra como argumento da escrita — é o que amarra as duas: o criar grava e depois chama recarregar, e o efeito de dependência da escrita é o que o aplicativo nunca precisa lembrar. É o mesmo desenho de cache invalidation que o React Query usa, com o nome invalidate para a operação de marcar o dado como velho.
O argumento nomeado invalidate descreve o que acontece: a gravação invalidate a leitura, e o recarregar seguinte é a rotina que vai buscar de novo. É o que resolve o bug mais comum do trimestre: "gravei e a lista não atualizou". Com as duas partes no mesmo hook, esse bug não tem como existir — a escrita que não chamar o recarregar teria que ser escrita de propósito.
O exemplo fecha o ciclo completo: grava uma tarefa, mostra que o banco passou a ter duas linhas e que a lista da tela mudou, e depois apaga e mostra a lista de novo. É o estado único funcionando — a lista que a tela desenha é sempre a que a consulta devolveu por último.
Exemplo
// O hook de dados: carrega, recarrega e devolve as tres coisas que a tela // precisa. A escrita e separada da leitura e recebe o `recarregar` como // argumento — e o que amarra as duas e essa chamada. const { useCallback, useEffect, useState } = require('react-native'); const banco = { tarefas: [ { id: 1, titulo: 'Revisar o WHERE', prazo: '2026-09-10', feita: 0 }, ] }; function buscarDoBanco(filtro) { let linhas = [...banco.tarefas]; if (filtro === 'pendentes') linhas = linhas.filter((l) => l.feita === 0); if (filtro === 'feitas') linhas = linhas.filter((l) => l.feita === 1); return linhas.sort((a, b) => (a.prazo > b.prazo ? 1 : -1)); } function useTarefas(filtro) { const [tarefas, setTarefas] = useState([]); const [carregando, setCarregando] = useState(true); const [erro, setErro] = useState(null); // `useCallback` devolve a mesma funcao enquanto `filtro` nao mudar const recarregar = useCallback(async () => { try { setCarregando(true); setErro(null); const linhas = buscarDoBanco(filtro); setTarefas(linhas); } catch (falha) { setErro(falha.message); } finally { setCarregando(false); } }, [filtro]); // depende de `recarregar`: mudar o filtro refaz a consulta useEffect(() => { recarregar(); }, [recarregar]); return { tarefas: tarefas, carregando: carregando, erro: erro, recarregar: recarregar }; } function useEscritaTarefas(recarregar) { const [atualizando, setAtualizando] = useState(false); async function criar(titulo, prazo) { setAtualizando(true); try { banco.tarefas.push({ id: banco.tarefas.length + 1, titulo: titulo, prazo: prazo, feita: 0 }); // a escrita sempre termina relendo: e o que garante que a tela // e o banco concordem await recarregar(); return { ok: true }; } catch (falha) { return { ok: false, erro: falha.message }; } finally { setAtualizando(false); } } async function apagar(id) { banco.tarefas = banco.tarefas.filter((t) => t.id !== id); await recarregar(); } return { criar: criar, apagar: apagar, atualizando: atualizando }; } // o que o hook devolve: os tres estados e o `recarregar` const leitura = useTarefas('pendentes'); console.log('o hook devolve as chaves:', Object.keys(leitura).join(', ')); console.log('filtro "pendentes" devolveu', buscarDoBanco('pendentes').length, 'linhas'); // a leitura separada da escrita const escrita = useEscritaTarefas(leitura.recarregar); console.log('a escrita devolve:', Object.keys(escrita).join(', ')); // o ciclo completo: gravar, reler, e a lista da tela ja vem do banco (async function () { const resultado = await escrita.criar('Enviar o relatorio', '2026-09-08'); console.log('criar devolveu:', JSON.stringify(resultado)); console.log('linhas no banco depois do INSERT:', banco.tarefas.length); console.log('linhas que a tela mostra:', lerComBanco()); await escrita.apagar(2); console.log('depois do DELETE:', lerComBanco()); })(); function lerComBanco() { return buscarDoBanco('pendentes').map((t) => t.titulo); }
Saída real
o hook devolve as chaves: tarefas, carregando, erro, recarregar
filtro "pendentes" devolveu 1 linhas
a escrita devolve: criar, apagar, atualizando
criar devolveu: {"ok":true}
linhas no banco depois do INSERT: 2
linhas que a tela mostra: [ 'Enviar o relatorio', 'Revisar o WHERE' ]
depois do DELETE: [ 'Revisar o WHERE' ]