Dia 11 — A consulta dentro do app: um hook de dados

Informatica · Conteudo · publicado em 05/10/2026
Dia 11 de 15

A consulta dentro do app: um hook de dados

Aula 1

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}
Aula 2

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' ]