Reagindo à entrada de dados com state
React oferece uma maneira declarativa de manipular a interface do usuário. Em vez de manipular diretamente partes individuais da UI, você descreve os diferentes estados em que seu componente pode estar e alterna entre eles em resposta à entrada do usuário. Isso é semelhante ao modo como os designers pensam sobre a UI.
Você aprenderá
- Como programação da UI difere entre declarativa e imperativa
- Como enumerar os diferentes estados visuais em que seu componente pode estar
- Como acionar as alterações entre os diferentes estados visuais a partir do código
Como a UI declarativa se compara à imperativa
Ao projetar interações de UI, você provavelmente pensa em como a UI muda em resposta às ações do usuário. Considere um formulário que permite que o usuário envie uma resposta:
- Quando você digita algo no formulário, o botão “Enviar” fica habilitado.
- Quando você pressiona “Enviar”, tanto o formulário quanto o botão ficam desativados e um loader aparece.
- Se a solicitação de rede for bem-sucedida, o formulário ficará oculto e a mensagem “Obrigado” aparecerá.
- Se a solicitação de rede falhar, uma mensagem de erro aparecerá e o formulário ficará habilitado novamente.
Na programação imperativa, o que foi dito acima corresponde diretamente a como você implementa a interação. Você precisa escrever as instruções exatas para manipular a interface do usuário, dependendo do que acabou de acontecer. Eis outra maneira de pensar sobre isso: imagine estar ao lado de alguém em um carro e dizer a essa pessoa, curva à curva, para onde ir.
Illustrated by Rachel Lee Nabors
Essa pessoa não sabe para onde você quer ir, apenas segue os seus comandos. (E se você errar as instruções, acabará no lugar errado!) É chamada de imperativa porque você precisa “comandar” cada elemento, desde o loader até o botão, dizendo ao computador como atualizar a interface do usuário.
Neste exemplo de programação imperativa de UI, o formulário é criado sem o React. Ele usa apenas o DOM do navegador:
async function handleFormSubmit(e) { e.preventDefault(); disable(textarea); disable(button); show(loadingMessage); hide(errorMessage); try { await submitForm(textarea.value); show(successMessage); hide(form); } catch (err) { show(errorMessage); errorMessage.textContent = err.message; } finally { hide(loadingMessage); enable(textarea); enable(button); } } function handleTextareaChange() { if (textarea.value.length === 0) { disable(button); } else { enable(button); } } function hide(el) { el.style.display = 'none'; } function show(el) { el.style.display = ''; } function enable(el) { el.disabled = false; } function disable(el) { el.disabled = true; } function submitForm(answer) { // Simula que está acessando a rede. return new Promise((resolve, reject) => { setTimeout(() => { if (answer.toLowerCase() == 'istambul') { resolve(); } else { reject(new Error('Bom palpite, mas resposta errada. Tente novamente!')); } }, 1500); }); } let form = document.getElementById('form'); let textarea = document.getElementById('textarea'); let button = document.getElementById('button'); let loadingMessage = document.getElementById('loading'); let errorMessage = document.getElementById('error'); let successMessage = document.getElementById('success'); form.onsubmit = handleFormSubmit; textarea.oninput = handleTextareaChange;
Manipular a UI de forma imperativa funciona bem em exemplos isolados, mas fica exponencialmente mais difícil de gerenciar em sistemas mais complexos. Imagine atualizar uma página cheia de formulários diferentes como esse. Adicionar um novo elemento de UI ou uma nova interação exigiria a verificação cuidadosa de todo o código existente para garantir que você não tenha introduzido um bug (por exemplo, esquecer de mostrar ou ocultar algo).
React foi criado para resolver esse problema.
Em React, você não manipula diretamente a UI, ou seja, você não ativa, desativa, mostra ou oculta componentes diretamente. Em vez disso, você declara o que deseja mostrar e React trata de como atualizar a UI. Pense em entrar em um táxi e dizer ao motorista para onde você quer ir, ao invés de dizer a ele exatamente onde virar. O trabalho do motorista é levá-lo até lá, e ele pode até conhecer alguns atalhos que você não considerou!
Illustrated by Rachel Lee Nabors
Pensando na UI de forma declarativa
Você viu acima como implementar um formulário de forma imperativa. Para entender melhor como pensar em React, você verá a seguir como reimplementar essa interface do usuário usando React:
- Identifique os diferentes estados visuais de seu componente
- Determine o que aciona essas mudanças no
state
- Represente o
state
na memória usandouseState
- Remova quaisquer variáveis não essenciais do
state
- Conecte os manipuladores de eventos para definir o
state
Etapa 1: Identificar os diferentes estados visuais do seu componente
Na ciência da computação, você pode ouvir falar que uma “máquina de estado” está em um de vários “estados”. Se você trabalha com um designer, pode ter visto modelos de diferentes “estados visuais”. O React está na interseção do design e da ciência da computação, portanto, essas duas ideias são fontes de inspiração.
Primeiro, você precisa visualizar todos os diferentes “estados” da UI que o usuário poderá ver:
- Empty (vazio): O formulário tem um botão “Enviar” desativado.
- Typing (digitando): O formulário tem um botão “Enviar” ativado.
- Submit (enviar): O formulário está completamente desativado. O loader é exibido.
- Sucess (successo): A mensagem “Obrigado” é exibida em vez de um formulário.
- Error (erro): Igual ao
state
de “digitando”, mas com uma mensagem de erro extra.
Assim como um designer, você desejará “simular” ou criar “mocks” para os diferentes estados antes de adicionar a lógica. Por exemplo, aqui está uma simulação para apenas a parte visual do formulário. Essa simulação é controlada por uma propriedade chamada status
com um valor padrão de 'empty'
:
export default function Form({ status = 'empty' }) { if (status === 'success') { return <h1>É isso mesmo!</h1> } return ( <> <h2>Questionário sobre cidades</h2> <p> Em qual cidade há um outdoor que transforma ar em água potável? </p> <form> <textarea /> <br /> <button> Enviar </button> </form> </> ) }
Você pode nomear essa propriedade como quiser, a nomenclatura não é importante. Tente editar status = 'empty'
para status = 'success'
para ver a mensagem de sucesso aparecer. A simulação permite que você itere rapidamente na interface do usuário antes de conectar qualquer lógica. Aqui está um protótipo mais detalhado do mesmo componente, ainda “controlado” pela propriedade status
:
export default function Form({ // Tente mudar para 'submitting', 'error' or 'success': status = 'empty' }) { if (status === 'success') { return <h1>É isso mesmo!</h1> } return ( <> <h2>Questionário sobre cidades</h2> <p> Em qual cidade há um outdoor que transforma ar em água potável? </p> <form> <textarea disabled={ status === 'submitting' } /> <br /> <button disabled={ status === 'empty' || status === 'submitting' }> Enviar </button> {status === 'error' && <p className="Error"> Bom palpite, mas resposta errada. Tente novamente! </p> } </form> </> ); }
Deep Dive
Se um componente tiver muitos estados visuais, pode ser conveniente mostrar todos eles em uma página:
import Form from './Form.js'; let statuses = [ 'empty', 'typing', 'submitting', 'success', 'error', ]; export default function App() { return ( <> {statuses.map(status => ( <section key={status}> <h4>Formulário ({status}):</h4> <Form status={status} /> </section> ))} </> ); }
Páginas como essa são geralmente chamadas de “guias de estilo vivos” ou ”storybooks“.
Etapa 2: Determinar o que aciona essas mudanças de estado
Você pode acionar atualizações de estado em resposta a dois tipos de entradas:
- Entradas humanas, como clicar em um botão, digitar em um campo, navegar em um link.
- Entradas de computador, como receber uma resposta de rede, a conclusão de um tempo limite, o carregamento de uma imagem.
Illustrated by Rachel Lee Nabors
Em ambos os casos, você deve definir variáveis de state para atualizar a UI. Para o formulário que você está desenvolvendo, será necessário alterar o state
em resposta a algumas entradas diferentes:
- Alterar a entrada de texto (humano) deve mudá-la do
state
Empty (vazio) para ostate
Typing (digitando) ou vice-versa, dependendo do fato de a caixa de texto estar vazia ou não. - Clicar no botão Enviar (humano) deve mudar para o
state
Submitting (enviando). - A resposta de rede bem-sucedida (computador) deve mudar para o
state
Success (sucesso). - A resposta de rede com falha (computador) deve mudar para o
state
Error (erro) com a mensagem de erro correspondente.
Para ajudar a visualizar esse fluxo, tente desenhar cada state
no papel como um círculo rotulado e cada mudança entre dois estados como uma seta. Você pode esboçar muitos fluxos dessa forma e resolver os bugs muito antes da implementação.
Etapa 3: Representar o state
na memória com useState
Em seguida, você precisará representar os estados visuais do seu componente na memória com useState
. A simplicidade é fundamental: cada state
é uma “peça móvel”, e você quer o menor número possível de “peças móveis”. Maior complexidade leva a mais bugs!
Comece com o state
que absolutamente precisa estar lá. Por exemplo, você precisará armazenar answer
para a entrada e error
(se existir) para armazenar o último erro:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
Em seguida, você precisará de uma variável de state
que represente qual dos estados visuais você deseja exibir. Geralmente, há mais de uma maneira de representar isso na memória, portanto, você precisará experimentar.
Se tiver dificuldade para pensar na melhor maneira imediatamente, comece adicionando um número suficiente de state
para ter certeza absoluta de que todos os estados visuais possíveis estão incluídos:
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
Sua primeira ideia provavelmente não será a melhor, mas tudo bem: refatorar o state
faz parte do processo!
Etapa 4: Remover todas as variáveis não essenciais do state
Você quer evitar a duplicação no conteúdo do state
para rastrear apenas o que é essencial. Gastar um pouco de tempo refatorando sua estrutura de state
tornará seus componentes mais fáceis de entender, reduzirá a duplicação e evitará significados não intencionais. Seu objetivo é prevenir os casos em que o state
na memória não representa nenhuma UI válida que você gostaria que o usuário visse. (Por exemplo, você nunca quer mostrar uma mensagem de erro e desativar a entrada ao mesmo tempo, ou o usuário não conseguirá corrigir o erro!)
Aqui estão algumas perguntas que você pode fazer sobre suas variáveis de state
:
- Por exemplo,
isTyping
(está digitando) eisSubmitting
(está enviando) não podem ser ambostrue
. Um paradoxo geralmente significa que ostate
não é suficientemente restrito. Há quatro combinações possíveis de dois booleanos, mas apenas três correspondem astate
válidos. Para remover ostate
“impossível”, você pode combiná-los em umstatus
que deve ser um dos três valores:'typing'
(digitando),'submitting'
(enviando) ou'success'
(sucesso). - A mesma informação já está disponível em outra variável de
state
? Outro paradoxo:isEmpty
(está vazio) eisTyping
(está digitando) não podem sertrue
ao mesmo tempo. Ao torná-las variáveis destate
separadas, você corre o risco de que elas fiquem dessincronizadas e causem bugs. Felizmente, você pode removerisEmpty
(está vazio) e, em vez disso, verificaranswer.length === 0
. - Você pode obter as mesmas informações do inverso de outra variável de
state
? OisError
(é erro) não é necessário porque você pode verificarerror !== null
em vez disso.
Após essa remoção, você fica com 3 (antes eram 7!) variáveis de state
essenciais:
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
Você sabe que eles são essenciais, pois não é possível remover nenhum deles sem prejudicar a funcionalidade.
Deep Dive
Essas três variáveis são uma representação suficientemente boa do state
desse formulário. Entretanto, ainda há alguns estados intermediários que não fazem sentido. Por exemplo, um error
não nulo não faz sentido quando status
é 'success'
. Para modelar o state
com mais precisão, você pode extraí-lo em um reducer Os reducers permitem unificar várias variáveis de state
em um único objeto e consolidar toda a lógica relacionada!
Etapa 5: Conecte os manipuladores de eventos para definir o state
Por fim, crie manipuladores de eventos que atualizem o state
. Abaixo está o formulário final, com todos os manipuladores de eventos conectados:
import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return <h1>É isso mesmo!</h1> } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <> <h2>Questionário sobre cidades</h2> <p> Em que cidade há um outdoor que transforma ar em água potável? </p> <form onSubmit={handleSubmit}> <textarea value={answer} onChange={handleTextareaChange} disabled={status === 'submitting'} /> <br /> <button disabled={ answer.length === 0 || status === 'submitting' }> Enviar </button> {error !== null && <p className="Error"> {error.message} </p> } </form> </> ); } function submitForm(answer) { // Simula que está acessando a rede. return new Promise((resolve, reject) => { setTimeout(() => { let shouldError = answer.toLowerCase() !== 'lima' if (shouldError) { reject(new Error('Bom palpite, mas resposta errada. Tente novamente!')); } else { resolve(); } }, 1500); }); }
Embora esse código seja mais longo do que o exemplo imperativo original, ele é muito menos frágil. Expressar todas as interações como alterações de state
permite que você introduza posteriormente novos estados visuais sem quebrar os existentes. Também permite que você altere o que deve ser exibido em cada state
sem alterar a lógica da própria interação.
Recap
- Programação declarativa significa descrever a UI para cada estado visual em vez de microgerenciá-la (imperativa).
- Ao desenvolver um componente:
- Identifique todos os seus estados visuais.
- Determine os acionadores humanos e computacionais para as mudanças de estado.
- Modele o
state
comuseState
. - Remova o
state
não essencial para evitar bugs e paradoxos. - Conecte os manipuladores de eventos para definir o
state
.
Challenge 1 of 3: Adicionar e remover uma classe CSS
Faça com que, ao clicar na imagem, a classe CSS background--active
seja removida da <div>
externa, mas a classe picture--active
seja adicionada ao <img>
. Clicar novamente no plano de fundo deve restaurar as classes CSS originais.
Visualmente, você deve esperar que clicar na imagem remova o plano de fundo roxo e destaque a borda da imagem. Clicar fora da imagem destaca o plano de fundo, mas remove o destaque da borda da imagem.
export default function Picture() { return ( <div className="background background--active"> <img className="picture" alt="Casas de arco-íris em Kampung Pelangi, Indonésia" src="https://i.imgur.com/5qwVYb1.jpeg" /> </div> ); }