Dia 13 — Formulário que grava e revê

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

Formulário que grava e revê

Aula 1

useReducer e estado que cresce

useReducer e estado que cresce

useState funciona enquanto o estado é uma coisa só. No primeiro

setEstado(valor) o estado vira o valor, no segundo vira a função, e o

useState decide entre os dois pela forma. O estado cresce até o dia em que

ele passa a ser um objeto com nome, e o set precisa decidir qual campo muda —

e aí o useState fica com a forma set(estado) => ({ ...estado, nome })

escrito dentro do componente, repetido em cada campo.

useReducer tira essa decisão do componente. O estado é uma coisa só, o set

vira dispatch, e uma função separada — o reducer — recebe o estado atual e

a ação, e devolve o próximo estado:

function reducer(estado, acao) {
  switch (acao.tipo) {
    case 'alterar':
      return { ...estado, [acao.campo]: acao.valor };
    case 'resetar':
      return estadoInicial;
    default:
      return estado;
  }
}

const [estado, despachar] = useReducer(reducer, estadoInicial);

O exemplo desta página tem um formulário de três campos — nome, email e

idade — e o estado inicial sai como { nome: '', email: '', idade: '' }. As

três alterações seguintes usam o mesmo caminho: uma ação alterar com

campo e valor. Nenhum set foi escrito no componente, e nenhum campo tem

código próprio. É a economia que o reducer promete.

A ação é um objeto com tipo e os dados da mudança, e nomear o tipo como

verbo no passado ou no infinitivo é o que torna o histórico legível. dispatch

não altera nada: ele entrega a ação ao reducer. É a distinção que elimina a

classe de bug em que dois set competem pelo mesmo estado no mesmo toque.

A primeira alteração já mostra o mecanismo inteiro: o estado mudou? true, e o

despachar devolve o estado anterior e o novo lado a lado. Alterar o e-mail

depois devolve "o nome continua: ana" — o campo que não estava na ação não foi

tocado, porque o Object.assign copia o estado inteiro e sobrescreve só a chave

indicada.

o reducer é puro

Puro significa: o reducer devolve sempre o mesmo resultado para a mesma

entrada, e não mexe em nada de fora. Ele não chama fetch, não grava no

armazenamento e não usa Date.now(). Isso parece restritivo até o dia em que

um bug exige voltar dois passos atrás: com reducer puro, dá para rodar a mesma

ação sobre o mesmo estado e chegar ao mesmo lugar, e o defeito se reproduz.

O exemplo prova a pureza com a mesma entrada duas vezes: a primeira chamada

devolve { nome: 'bruno', email: '[email protected]', idade: '' } e a segunda

devolve exatamente o mesmo objeto. A entrada não foi alterada no caminho — o

nome continua "bruno" e a idade continua vazia. Um reducer que mutasse o

estado devolveria algo diferente na segunda vez.

O default devolvendo estado é obrigatório por um motivo prático: ação

desconhecida não pode quebrar a tela, e o reducer que devolve undefined no

default zera o estado inteiro. É o defeito mais caro de useReducer, porque

a tela fica vazia e a causa está longe do lugar onde o erro aparece.

Os dois console do default testam isso com duas ações diferentes: uma ação

de tipo "inexistente" e uma ação sem tipo nenhum. As duas devolvem o estado

inicial intacto. É a mesma proteção do parágrafo, medida: uma ação nova — de uma

biblioteca, de um dispatch antigo que sobrou — não derruba o formulário.

Estado inicial vem no segundo argumento do useReducer e o reducer é quem

devolve o estado vazio no resetar — o estadoInicial precisa existir como

constante fora, para que a linha do reset seja return estadoInicial e não

return {}.

O ciclo do reset no exemplo fecha o parágrafo: depois de submeter, o estado tem

os três campos preenchidos mais enviado: true; depois de resetar, os campos

voltam ao vazio e enviado volta a false. E o console confirma com

"os tres campos voltaram ao vazio: true". O `Object.assign({}, estadoInicial,

{ enviado: false })` reaproveita a constante — o que faz o reset funcionar em

um case só e não em três.

A árvore fecha o argumento com a parte que o reducer não resolve sozinho: o

componente recebe o estado por prop e mostra o value dos campos a partir dele.

Nada é escrito dentro do componente, e o último console confere que o valor

que ele recebe depois do reset é a string vazia — o mesmo estado que o reducer

devolveu.

Exemplo

// `useReducer` tira do componente a decisao de qual campo muda: o
// componente despacha uma ACAO e o reducer devolve o proximo estado. O
// exemplo e um formulario com tres campos.
const { View, Text, TextInput, useReducer, StyleSheet } = require('react-native');

// (1) o estado inicial e o reducer: duas funcoes puras, fora do componente.
const estadoInicial = { nome: '', email: '', idade: '' };

function reducer(estado, acao) {
  switch (acao.tipo) {
    case 'alterar':
      // um unico caminho para os tres campos
      return Object.assign({}, estado, { [acao.campo]: acao.valor });
    case 'submeter':
      return Object.assign({}, estado, { enviado: true });
    case 'resetar':
      return Object.assign({}, estadoInicial, { enviado: false });
    default:
      return estado;      // sem `default`, uma acao nova zera o estado
  }
}

// (2) `dispatch` nao altera nada: entrega a acao ao reducer.
function criarDespachador() {
  let estado = estadoInicial;
  return {
    despachar(acao) {
      const anterior = estado;
      estado = reducer(estado, acao);
      return { anterior, agora: estado, mudou: anterior !== estado };
    },
    ler() {
      return estado;
    },
  };
}

const loja = criarDespachador();
console.log('estado inicial:', loja.ler());

let passo = loja.despachar({ tipo: 'alterar', campo: 'nome', valor: 'ana' });
console.log('\nalterar nome ->', passo.agora.nome, '| o estado mudou?', passo.mudou);
passo = loja.despachar({ tipo: 'alterar', campo: 'email', valor: '[email protected]' });
console.log('alterar e-mail ->', passo.agora.email, '| o nome continua:', passo.agora.nome);
passo = loja.despachar({ tipo: 'alterar', campo: 'idade', valor: '34' });
console.log('alterar idade ->', passo.agora.idade);
console.log('estado com os tres campos:', loja.ler());

// (3) o reducer e puro: mesma entrada, mesma saida.
const entrada = { nome: 'bruno', email: '', idade: '' };
const acao = { tipo: 'alterar', campo: 'email', valor: '[email protected]' };
console.log('\nprimeira vez:', reducer(entrada, acao));
console.log('segunda vez: ', reducer(entrada, acao));
console.log('mesma saida nas duas: o reducer nao mexe em nada de fora');

// (4) o `default` devolve o estado: acao desconhecida nao quebra a tela.
console.log('\nacao desconhecida:', reducer(estadoInicial, { tipo: 'inexistente' }));
console.log('acao sem tipo:', reducer(estadoInicial, {}));

// (5) resetar devolve o estado inicial da constante, nao um objeto vazio.
const depoisDeEnviar = loja.despachar({ tipo: 'submeter' });
console.log('\ndepois de submeter:', depoisDeEnviar.agora);
const depoisDeLimpar = loja.despachar({ tipo: 'resetar' });
console.log('depois de resetar:', depoisDeLimpar.agora);
console.log('os tres campos voltaram ao vazio:', depoisDeLimpar.agora.nome === '' && depoisDeLimpar.agora.email === '');

// (6) a arvore: o componente despacha, e nao escreve o estado.
const estilos = StyleSheet.create({ campo: { borderWidth: 1, borderColor: '#d0d7de', padding: 8 } });
function FormularioComReducer(props) {
  return (
    <View>
      <TextInput style={estilos.campo} value={props.estado.nome} placeholder="nome" />
      <TextInput style={estilos.campo} value={props.estado.email} placeholder="e-mail" />
      <Text>{props.estado.enviado ? 'enviado' : 'nao enviado'}</Text>
    </View>
  );
}

const arvore = FormularioComReducer({ estado: loja.ler() });
console.log('\ntipo da arvore:', arvore.type, '| filhos:', arvore.props.children.length);
console.log('o valor do primeiro campo:', arvore.props.children[0].props.value);
console.log('o estado depois de resetar e o que o componente recebe:',
  FormularioComReducer({ estado: loja.ler() }).props.children[0].props.value === '');

Saída real

estado inicial: { nome: '', email: '', idade: '' }

alterar nome -> ana | o estado mudou? true
alterar e-mail -> [email protected] | o nome continua: ana
alterar idade -> 34
estado com os tres campos: { nome: 'ana', email: '[email protected]', idade: '34' }

primeira vez: { nome: 'bruno', email: '[email protected]', idade: '' }
segunda vez:  { nome: 'bruno', email: '[email protected]', idade: '' }
mesma saida nas duas: o reducer nao mexe em nada de fora

acao desconhecida: { nome: '', email: '', idade: '' }
acao sem tipo: { nome: '', email: '', idade: '' }

depois de submeter: { nome: 'ana', email: '[email protected]', idade: '34', enviado: true }
depois de resetar: { nome: '', email: '', idade: '', enviado: false }
os tres campos voltaram ao vazio: true

tipo da arvore: View | filhos: 3
o valor do primeiro campo: 
o estado depois de resetar e o que o componente recebe: true
Aula 2

Revisar o que foi preenchido

Revisar o que foi preenchido

Revisar é mostrar o que foi gravado, e a tela de revisão é a que mais denuncia

inconsistência de estado — porque é ela que junta o que veio de várias

operações: o que foi criado agora, o que foi carregado do servidor, e o que

ficou de uma edição anterior.

O estado vazio é o primeiro caso, e tem uma regra que evita metade das telas

mortas: lista vazia é um estado, não um erro. São quatro estados distintos e

a tela precisa dos quatro — carregando, vazio, com erro e com dado. A tela que

só sabe desenhar o estado com dado passa a lista vazia como "carregando para

sempre", e o usuário fica sem saber se o app quebrou ou se ele não tem

registro.

O exemplo desta página escreve os quatro estados num array e a função telaDo

escolhe entre eles com três condições. As quatro linhas de saída são a prova de

que são quatro e não dois: carregando sem dado, pronto sem registro, pronto com

registro e com erro. O erro aparece mesmo com dado presente — o que é a parte

que costuma faltar, porque a condição de erro costuma ser testada só quando a

lista está vazia.

Remover tem dois passos, e o passo que se esquece é o que produz a lista

incoerente: confirmar, depois remover. Alert.alert com dois botões é o

componente que confirma — e a alternativa sem alerta é uma tela de confirmação,

que em aplicativo sério é o caminho melhor, porque dá para mostrar o que está

sendo removido.

A atualização da lista depois de remover precisa ser feita pelo id, não pela

posição. filtrar por id é o comportamento certo; remover pelo índice quebra

quando a lista já mudou entre o clique e a confirmação.

O exemplo remove pelo id e o resultado traz as três peças: o nome do que foi

removido, a contagem antes e depois, e os id que sobraram — [ 'p41', 'p43' ].

E o caso do registro inexistente sai com removido: null e as duas contagens

iguais: a função não falhou, não encontrou alvo. É a diferença entre "não existe

mais" e "não deu para remover", e uma lista reativa precisa respeitar as duas.

editar e recarregar

Editar é carregar o registro, abrir o formulário com os valores no lugar e

gravar por cima. O detalhe que evita defeito é comparar antes de gravar: sem

diferença, não há por que chamar o servidor, e a tela não precisa recarregar

tudo. Com diferença, o id é o que faz o registro ser o mesmo e não um

novo — gravar com id ausente é o que cria duplicata em cada edição.

O exemplo grava pelo id e compara as duas respostas: gravado: true para o

registro existente, gravado: false com o motivo "registro nao existe" para o

p99. A segunda resposta é a que impede a duplicata — sem id, o servidor

receberia um registro novo em vez de uma edição.

A comparação antes de gravar é uma função sobre os campos, e o exemplo mede os

dois lados: sem diferença, false; com o total mudado, true. O `campo !==

'id' dentro do some é o detalhe — o id` é o mesmo registro, não uma

alteração. Sem essa exclusão, toda edição seria detectada como mudança.

refetch é a saída de emergência, e é melhor que a tela mostrar dado velho

para sempre. Mas ele tem custo: recarregar a lista inteira perde a posição da

rolagem e o filtro digitado. Por isso ele é botão de "atualizar" e não

comportamento automático.

O exemplo mede esse custo em um número: p41 estava na posição 0 e passa para

a posição 1 depois do refetch, porque a resposta nova trouxe outro registro

antes dele. A rolagem estava em { posicao: 12, filtro: 'ana' } e o console

completa que ela e o filtro só existem se forem guardados a parte. A posição

muda sozinha; o resto precisa ser salvo por quem recarrega.

o estado que sobrevive à tela

Quando a tela sai e volta, o estado é recriado do zero, e é por isso que o

refetch existe. Se o dado precisa sobreviver, ele vai para um lugar acima da

tela — o cache do serviço ou um contexto. A alternativa mais barata, e a que

resolve o caso comum de "voltei e perdi o que digitei", é guardar o formulário

num estado só na navegação, passando os valores por parâmetro de rota em vez do

registro inteiro: o valor é pequeno, é estável e não expira.

A árvore do exemplo trata o estado vazio como um ramo da própria tela, e o

retorno antecipado resolve: com dados são 2 filhos, e sem nenhum registro a

tela devolve um Text cujo conteúdo é "nenhum registro ainda". O return antes

do map é o que impede a lista de tentar desenhar itens inexistentes — e o que

permite que o texto do estado vazio seja escrito uma vez só, no lugar onde ele

pertence.

Exemplo

// Tela de revisao: os quatro estados (carregando, vazio, erro, com
// dado), remocao pelo `id`, e o que `refetch` custa quando a lista e
// recarregada. O exemplo opera sobre a lista em memoria.
const { View, Text, Pressable, StyleSheet } = require('react-native');

// (1) os quatro estados que a tela precisa saber distinguir.
const ESTADOS = ['carregando', 'vazio', 'erro', 'com dado'];

function telaDo(estado, temDados) {
  if (estado.carregando && !temDados) return ESTADOS[0];
  if (estado.erro) return ESTADOS[2];
  if (!temDados) return ESTADOS[1];
  return ESTADOS[3];
}

console.log('estados possiveis:', ESTADOS);
console.log('carregando sem dado:   ', telaDo({ carregando: true, erro: null }, false));
console.log('pronto sem registro:  ', telaDo({ carregando: false, erro: null }, false));
console.log('pronto com registro:  ', telaDo({ carregando: false, erro: null }, true));
console.log('com erro:             ', telaDo({ carregando: false, erro: 'nao leu' }, true));
console.log('lista vazia e um estado, nao um erro');

// (2) a lista e as operacoes sobre ela.
let pedidos = [
  { id: 'p41', cliente: 'ana', total: 90 },
  { id: 'p42', cliente: 'bruno', total: 45 },
  { id: 'p43', cliente: 'carla', total: 120 },
];

function remover(id) {
  const alvo = pedidos.find((p) => p.id === id);
  const antes = pedidos.length;
  pedidos = pedidos.filter((p) => p.id !== id);      // remove pelo `id`
  return { removido: alvo ? alvo.cliente : null, antes, depois: pedidos.length };
}

function editar(id, mudanca) {
  const existe = pedidos.some((p) => p.id === id);
  if (!existe) return { gravado: false, motivo: 'registro nao existe' };
  pedidos = pedidos.map((p) => (p.id === id ? Object.assign({}, p, mudanca) : p));
  return { gravado: true, motivo: null };
}

console.log('\nconfirmar antes de remover: o id vem do item, nao da posicao');
console.log('confirmacao:', 'remover p42?', '| o texto da confirmacao traz o cliente:',
  pedidos.find((p) => p.id === 'p42').cliente);
const r1 = remover('p42');
console.log('depois de remover:', r1, '| ids:', pedidos.map((p) => p.id));

console.log('remover o que nao existe:', remover('p99'));

console.log('\neditar: o `id` e o que garante que e o mesmo registro');
console.log(editar('p41', { total: 120 }));
console.log(editar('p99', { total: 10 }));
console.log('sem `id` na gravacao, cada edicao criaria um registro novo');

// (3) comparar antes de gravar: sem diferenca, nao ha por que chamar o servidor.
function mudou(antes, depois) {
  return Object.keys(antes).some((campo) => campo !== 'id' && antes[campo] !== depois[campo]);
}
const original = pedidos[0];
console.log('\ncomparando antes de gravar:');
console.log('sem diferenca:', mudou(original, Object.assign({}, original)));
console.log('com diferenca:', mudou(original, Object.assign({}, original, { total: 300 })));

// (4) o que `refetch` custa.
function comPosicao(lista, alvo) {
  return lista.findIndex((p) => p.id === alvo);
}
const posicaoAntes = comPosicao(pedidos, 'p41');
const rolagem = { posicao: 12, filtro: 'ana' };
console.log('\nantes do refetch: posicao na lista', posicaoAntes, '| rolagem', JSON.stringify(rolagem));
const recarregada = [
  { id: 'p40', cliente: 'dani', total: 10 },
  { id: 'p41', cliente: 'ana', total: 90 },
  { id: 'p43', cliente: 'carla', total: 120 },
];
console.log('depois do refetch: posicao na lista', comPosicao(recarregada, 'p41'));
console.log('a posicao mudou, e a rolagem e o filtro so existem se forem guardados a parte');
console.log('`refetch` e o botao de atualizar, nao comportamento automatico');

// (5) a arvore da tela de revisao, com o estado vazio junto.
const estilos = StyleSheet.create({ linha: { padding: 12 }, vazio: { padding: 24, textAlign: 'center' } });
function TelaRevisao(props) {
  if (props.itens.length === 0) {
    return <Text style={estilos.vazio}>nenhum registro ainda</Text>;
  }
  return (
    <View>
      {props.itens.map((item) => (
        <View key={item.id}>
          <Text style={estilos.linha}>{item.cliente}</Text>
          <Pressable onPress={() => props.aoRemover(item.id)}>
            <Text>remover</Text>
          </Pressable>
        </View>
      ))}
    </View>
  );
}

const comDados = TelaRevisao({ itens: pedidos, aoRemover: () => {} });
console.log('\ncom dados, filhos:', comDados.props.children.length);
const semDados = TelaRevisao({ itens: [], aoRemover: () => {} });
console.log('vazio, filhos:', semDados.props.children.length, '| texto:', semDados.props.children);
console.log('o estado vazio e um ramo da propria tela, com o que mostrar nele');

Saída real

estados possiveis: [ 'carregando', 'vazio', 'erro', 'com dado' ]
carregando sem dado:    carregando
pronto sem registro:   vazio
pronto com registro:   com dado
com erro:              erro
lista vazia e um estado, nao um erro

confirmar antes de remover: o id vem do item, nao da posicao
confirmacao: remover p42? | o texto da confirmacao traz o cliente: bruno
depois de remover: { removido: 'bruno', antes: 3, depois: 2 } | ids: [ 'p41', 'p43' ]
remover o que nao existe: { removido: null, antes: 2, depois: 2 }

editar: o `id` e o que garante que e o mesmo registro
{ gravado: true, motivo: null }
{ gravado: false, motivo: 'registro nao existe' }
sem `id` na gravacao, cada edicao criaria um registro novo

comparando antes de gravar:
sem diferenca: false
com diferenca: true

antes do refetch: posicao na lista 0 | rolagem {"posicao":12,"filtro":"ana"}
depois do refetch: posicao na lista 1
a posicao mudou, e a rolagem e o filtro so existem se forem guardados a parte
`refetch` e o botao de atualizar, nao comportamento automatico

com dados, filhos: 2
vazio, filhos: 21 | texto: nenhum registro ainda
o estado vazio e um ramo da propria tela, com o que mostrar nele