Pular para o conteúdo

d

Um estado mais complexo e depuração de aplicações React

Um estado complexo (complex state)

Em nosso exemplo anterior, o estado da aplicação era simples, pois consistia em apenas um número inteiro. E se a nossa aplicação precisar de um estado mais complexo?

Na maioria dos casos, a maneira mais fácil e melhor de fazer isso é usando a função useState múltiplas vezes para criar "pedaços" separados de estado.

No código a seguir, criamos dois pedaços de estado para a aplicação, chamados esquerda e direita, ambos com o valor inicial 0:

const App = () => {
  const [esquerda, setEsquerda] = useState(0) 
  const [direita, setDireita] = useState(0) 

  return (
    <div>
      {esquerda}
      <button onClick={() => setEsquerda(esquerda + 1)}>
        Esquerda
      </button>
      <button onClick={() => setDireita(direita + 1)}>
        Direita
      </button>
      {direita}
    </div>
  )
}

O componente têm acesso às funções setEsquerda e setDireita, que podem ser usadas para atualizar os dois pedaços de estado.

O estado ou um pedaço de estado do componente pode ser de qualquer tipo. Poderíamos implementar a mesma funcionalidade salvando a contagem de cliques tanto dos botões "esquerda" quanto "direita" em um único objeto:

{
  esquerda: 0,
  direita: 0
}

Nesse caso, a aplicação ficaria assim:

const App = () => {
  const [cliques, setCliques] = useState({ 
    esquerda: 0, direita: 0
  })

  const handleCliqueEsquerda = () => {
    const novosCliques = { 
      esquerda: cliques.esquerda + 1, 
      direita: cliques.direita 
    }
    setCliques(novosCliques)
  }

  const handleCliqueDireita = () => {
    const novosCliques = { 
      esquerda: cliques.esquerda, 
      direita: cliques.direita + 1 
    }
    setCliques(novosCliques)
  }

  return (
    <div>
      {cliques.esquerda}
      <button onClick={handleCliqueEsquerda}>Esquerda</button>
      <button onClick={handleCliqueDireita}>Direita</button>
    </div>
  )
}

Agora, o componente tem apenas um único pedaço de estado, e os gerenciadores de eventos precisam cuidar da mudança do estado inteiro da aplicação.

O formato do gerenciador de evento parece confuso aqui. Quando o botão da esquerda é clicado, a seguinte função é chamada:

const handleCliqueEsquerda = () => {
  const novosCliques = { 
    esquerda: cliques.esquerda + 1, 
    direita: cliques.direita 
  }
  setCliques(novosCliques)
}

O objeto a seguir é definido como o novo estado da aplicação:

{
  esquerda: cliques.esquerda + 1,
  direita: cliques.direita
}

O novo valor da propriedade esquerda agora é o mesmo que o valor de esquerda + 1 do estado anterior, e o valor da propriedade direita é o mesmo que o valor da propriedade direita do estado anterior.

Podemos definir mais claramente o novo objeto de estado usando a (sintaxe de espalhamento) (Spread syntax (...)) que foi adicionada à especificação da linguagem no verão de 2018:

const handleCliqueEsquerda = () => {
  const novosCliques = { 
    ...cliques, 
    esquerda: cliques.esquerda + 1 
  }
  setCliques(novosCliques)
}

const handleCliqueDireita = () => {
    ...cliques, 
    direita: cliques.direita + 1 
  }
  setCliques(novosCliques)
}

A sintaxe pode parecer um tanto estranha no começo. Na prática, { ...cliques } cria um novo objeto que tem cópias de todas as propriedades do objeto cliques. Quando discriminamos uma propriedade específica — por exemplo, direita em { ...cliques, direita: 1 }, o valor da propriedade direita no novo objeto será 1.

No exemplo acima, este trecho:

{ ...cliques, direita: cliques.direita + 1 }

cria uma cópia do objeto cliques, onde o valor da propriedade direita é aumentado em 1.

Não é necessário atribuir o objeto a uma variável nos gerenciadores de eventos, e podemos simplificar as funções da seguinte maneira:

const handleCliqueEsquerda = () =>
  setCliques({ ...cliques, esquerda: cliques.esquerda + 1 })

const handleCliqueDireita = () =>

Alguns leitores podem estar se perguntando o motivo de não termos atualizado o estado diretamente, desta forma:

const handleCliqueEsquerda = () => {
  cliques.esquerda++
  setCliques(cliques)
}

A aplicação parece funcionar. Entretanto, em React, é proibido mudar (mutate) diretamente o estado, já que pode resultar em efeitos colaterais inesperados. A mudança de estado sempre tem que ser feita pela definição/atribuição do estado a um novo objeto. Se as propriedades do objeto de estado anterior não forem alteradas, podem simplesmente ser copiadas, o que se faz copiando essas propriedades em um novo objeto e definindo-o como o novo estado.

Armazenar todo o estado em um único objeto de estado é uma má escolha para esta aplicação, especificamente; não há qualquer benefício aparente, e a aplicação resultante fica muito mais complexa. Neste caso, armazenar os contadores de cliques em pedaços separados de estado é uma escolha muito mais adequada.

Há situações em que pode ser benéfico armazenar um pedaço de estado da aplicação em uma estrutura de dados mais complexa. A documentação oficial de React contém algumas orientações úteis sobre o assunto.

Gerenciando Arrays

Vamos adicionar um pedaço de estado à nossa aplicação contendo o array todosOsCliques, que lembra cada clique que ocorreu na aplicação.

const App = () => {
  const [esquerda, setEsquerda] = useState(0)
  const [direita, setDireita] = useState(0)
  const [todosOsCliques, setTodos] = useState([])
  const handleCliqueEsquerda = () => {    setTodos(todosOsCliques.concat('E'))    setEsquerda(esquerda + 1)  } 
  const handleCliqueDireita = () => {    setDireita(direita + 1)  }
  return (
    <div>
      {esquerda}
      <button onClick={handleCliqueEsquerda}>Esquerda</button>
      <button onClick={handleCliqueDireita}>Direita</button>
      <p>{todosOsCliques.join(' ')}</p>    </div>
  )
}

Cada clique é armazenado em um pedaço separado de estado chamado todosOsCliques, que é inicializado como um array vazio:

const [todosOsCliques, setTodos] = useState([])

Quando o botão Esquerda é clicado, adicionamos a letra E ao array todosOsCliques:

const handleCliqueEsquerda = () => {
  setTodos(todosOsCliques.concat('E'))
  setEsquerda(esquerda + 1)
}

O pedaço de estado armazenado em todosOsCliques agora é definido para ser um array que contém todos os itens do array anterior mais a letra E. O método concat (concatenar) adiciona o novo item ao array, que não muda o array existente, mas sim retorna uma nova cópia do array com o item adicionado a ele.

Como mencionado anteriormente, também é possível em JavaScript adicionar itens a um array com o método push (Significa, literalmente, "empurrar", "apertar", "pressionar". Porém, nestes termos, o método push() ADICIONA um ou mais elementos ao final de um array e retorna o novo comprimento desse array). Se adicionarmos o item "empurrando-o" para o array todosOsCliques e então atualizando o estado, a aplicação ainda aparentará funcionar:

const handleCliqueEsquerda = () => {
  todosOsCliques.push('E')
  setTodos(todosOsCliques)
  setEsquerda(esquerda + 1)
}

No entanto, não faça isso. Como mencionado anteriormente, o estado dos componentes em React, tal como todosOsCliques, não devem ser mudados diretamente. Mesmo se mudando o estado parecer funcionar em alguns casos, tal decisão pode levar a erros no código muito difíceis de depurar.

Vamos olhar mais de perto em como o clique é renderizado na página:

const App = () => {
  // ...

  return (
    <div>
      {esquerda}
      <button onClick={handleCliqueEsquerda}>Esquerda</button>
      <button onClick={handleCliqueDireita}>Direita</button>
      {direita}
      <p>{todosOsCliques.join(' ')}</p>    </div>
  )
}

Chamamos o método join (juntar, conectar) no array todosOsCliques que une todos os itens em uma única string, separados pela string passada como parâmetro da função, que no caso é um espaço vazio.

A atualização do estado é assíncrona

Vamos expandir a aplicação para que ela mantenha o controle do número total de cliques nos botões no estado total, cujo valor é sempre atualizado quando os botões são pressionados:

const App = () => {
  const [esquerda, setEsquerda] = useState(0)
  const [direita, setDireita] = useState(0)
  const [todosOsCliques, setTodos] = useState([])
  const [total, setTotal] = useState(0)
  const handleCliqueEsquerda = () => {
    setTodos(todosOsCliques.concat('E'))
    setEsquerda(esquerda + 1)
    setTotal(esquerda + direita)  }

  const handleCliqueDireita = () => {
    setDireita(direita + 1)
    setTotal(esquerda + direita)  }

  return (
    <div>
      {esquerda}
      <button onClick={handleCliqueEsquerda}>Esquerda</button>
      <button onClick={handleCliqueDireita}>Direita</button>
      <p>{todosOsCliques.join(' ')}</p>
      <p>Total {total}</p>    </div>
  )
}

A solução não funciona corretamente:

o navegador mostrando 2 left|right 1, RLL total 2

Por alguma razão, o total de cliques nos botões está sempre um clique atrás do valor real.

Vamos adicionar alguns comandos console.log ao gerenciador de eventos:

const App = () => {
  // ...
  const handleCliqueEsquerda = () => {
    setTodos(todosOsCliques.concat('E'))
    console.log('clique esquerdo anterior', esquerda)    setEsquerda(esquerda + 1)
    console.log('clique esquerdo posterior', esquerda)    setTotal(esquerda + direita)
  }

  // ...
}

O console revela o problema:

o console das ferramentas do desenvolvedor exibe left before 4 and left after 4

Embora um novo valor tenha sido definido para esquerda chamando setEsquerda(esquerda + 1), o valor antigo ainda está lá, apesar da atualização! Por causa disso, a tentativa de contar o número de cliques nos botões produz um resultado menor do que o correto:

setTotal(esquerda + direita) 

O motivo para isso é que uma atualização de estado no React acontece assincronicamente (asynchronously), ou seja, não imediatamente, mas "em algum momento" antes que o componente seja renderizado novamente.

Podemos consertar a aplicação da seguinte forma:

const App = () => {
  // ...
  const handleCliqueEsquerda = () => {
    setTodos(todosOsCliques.concat('E'))
    const atualizaEsquerda = esquerda + 1
    setEsquerda(atualizaEsquerda)
    setTotal(atualizaEsquerda + direita)
  }

  // ...
}

Assim, o número de cliques nos botões é agora, de forma definitiva, baseado no número correto de cliques no botão esquerdo.

Renderização Condicional

Vamos modificar nossa aplicação para que a renderização do histórico de cliques seja gerenciada por um novo componente chamado Historico:

const Historico = (props) => {  if (props.todosOsCliques.length === 0) {    return (      <div>        Clique em um dos botões para usar a aplicação!      </div>    )  }  return (    <div>      Histórico de cliques nos botões: {props.todosOsCliques.join(' ')}    </div>  )}
const App = () => {
  // ...

  return (
    <div>
      {esquerda}
      <button onClick={handleCliqueEsquerda}>Esquerda</button>
      <button onClick={handleCliqueDireita}>Direita</button>
      {direita}
      <Historico todosOsCliques={todosOsCliques} />    </div>
  )
}

Agora, o comportamento do componente depende se algum dos botões foi clicado ou não. Se não, ou seja, o array todosOsCliques estando vazio, o componente renderiza um elemento "div" com algumas instruções:

<div>Clique em um dos botões para usar a aplicação!</div>

E em todos os outros casos, o componente renderiza o histórico de cliques:

<div>
  Histórico de cliques nos botões: {props.todosOsCliques.join(' ')}
</div>

O componente Historico renderiza elementos React completamente diferentes dependendo do estado da aplicação. Isso é chamado de renderização condicional (conditional rendering).

React também oferece muitas outras formas de fazer renderização condicional. Veremos isso na prática na Parte 2.

Vamos fazer mais uma modificação a nossa aplicação, refatorando-a para usar o componente Botao que definimos anteriormente:

const Historico = (props) => {
  if (props.todosOsCliques.length === 0) {
    return (
      <div>
        Clique em um dos botões para usar a aplicação!
      </div>
    )
  }

  return (
    <div>
      Histórico de cliques nos botões: {props.todosOsCliques.join(' ')}
    </div>
  )
}

const Botao = ({ handleClique, texto }) => (  <button onClick={handleClique}>    {texto}  </button>)
const App = () => {
  const [esquerda, setEsquerda] = useState(0)
  const [direita, setDireita] = useState(0)
  const [todosOsCliques, setTodos] = useState([])

  const handleCliqueEsquerda = () => {
    setTodos(todosOsCliques.concat('E'))
    setEsquerda(esquerda + 1)
  }

  const handleCliqueDireita = () => {
    setDireita(direita + 1)
  }

  return (
    <div>
      {esquerda}
      <Botao handleClique={handleCliqueEsquerda} texto='Esquerda' />      <Botao handleClique={handleCliqueDireita} texto='Direita' />      {direita}      <Historico todosOsCliques={todosOsCliques} />    </div>  )}

React antigo

Neste curso, usamos o state hook ("gancho de estado") para adicionar estado aos nossos componentes React, que faz parte das versões mais recentes da biblioteca e está disponível a partir da versão 16.8.0 em diante. Antes da adição dos hooks, não havia maneira de adicionar estado a componentes funcionais. Componentes que precisavam de estado tinham que ser definidos como componentes de classe, usando a sintaxe de classe JavaScript.

Neste curso, fizemos a decisão um pouco radical de usar exclusivamente hooks desde o primeiro dia, para garantir que estamos aprendendo as variações atuais e futuras de React. Embora os componentes funcionais sejam o futuro da biblioteca, ainda é importante aprender a sintaxe de classe, já que existem bilhões de linhas de código React legado que você pode acabar fazendo manutenção algum dia. O mesmo se aplica à documentação e exemplos de React que você pode encontrar na internet.

Vamos aprender mais sobre componentes de classe React mais tarde no curso.

Depuração de aplicações React

Grande parte do tempo de um desenvolvedor é gasto na depuração e na leitura de códigos existentes. De vez em quando, conseguimos escrever uma ou duas linhas de código novo, mas grande parte do nosso tempo é gasto tentando descobrir por que algo está quebrado ou como algo funciona. Boas práticas e ferramentas de depuração são extremamente importantes por esta razão.

Felizmente para nós, React é uma biblioteca extremamente amigável para com os desenvolvedores quando se trata de depuração.

Antes de continuarmos, vamos nos lembrar de uma das regras mais importantes do desenvolvimento web.

A primeira regra do desenvolvimento web

Mantenha o Console do navegador aberto o tempo todo.

A guia Console em particular deve estar sempre aberta, a menos que haja uma razão específica para visualizar outra guia.

Mantenha tanto o seu código quanto a página web abertos juntos o tempo todo.

Se e quando seu código não compilar e seu navegador brilhar igual uma árvore de Natal:

captura de tela do código

não escreva nenhuma linha de código a mais, mas encontre e corrija imediatamente o problema. Ainda não aconteceu na história da programação de o código que não estivesse compilando começasse a funcionar após a adição de mais linhas de código. Duvido que tal evento ocorra durante este curso também.

A depuração (debug) "old-school", baseada na impressão no Console, é sempre uma das melhores opções. Se o componente

const Botao = ({ handleClique, texto }) => (
  <button onClick={handleClique}>
    {texto}
  </button>
)

não estiver funcionando como desejado, é útil começar a imprimir suas variáveis ​​no console. Para que isso funcione, devemos transformar nossa função na forma menos compactada e receber todo o objeto "props" sem desestruturá-lo de forma imediata:

const Botao = (props) => { 
  console.log(props)  const { handleClique, texto } = props
  return (
    <button onClick={handleClique}>
      {texto}
    </button>
  )
}

Isso revelará imediatamente se, por exemplo, um dos atributos foi escrito incorretamente ao usar o componente.

Obs.: Quando você usar console.log para depuração, não combine objetos (objects) do jeito Java de se fazer usando o operador de adição. Em vez de escrever

console.log('o valor de props é ' + props)

separe as coisas que você deseja registrar no console com uma vírgula:

console.log('o valor de props é', props)

Se você usar o jeito Java de concatenar uma string com um objeto, aparecerá uma mensagem de log muito pouco informativa:

o valor de props é [object Object]

Registrar a saída no console não é de maneira alguma a única forma de depurar nossas aplicações. Você pode pausar a execução do código da sua aplicação no depurador (debugger) no Console do Desenvolvedor do Chrome, escrevendo o comando debugger em qualquer lugar do seu código.

A execução será pausada assim que chegar a um ponto onde o comando debugger for executado:

debugger pausado na Ferramenta do Desenvolvedor

Ao ir para a guia Console, é fácil inspecionar o estado atual das variáveis:

captura de tela de inspeção de console

Uma vez que a causa do erro é descoberta, é possível remover o comando debugger e atualizar a página.

O depurador também nos permite executar nosso código linha por linha com os controles encontrados na parte direita da guia Fontes (Sources).

Você também pode acessar o depurador sem o comando debugger, adicionando pontos de interrupção na guia Fontes (Sources). Inspecionar os valores das variáveis do componente pode ser feito na seção Escopo (Scope):

exemplo de ponto de interrupção nas ferramentas do desenvolvedor

É extremamente recomendado adicionar a extensão React developer tools ao Chrome. Ele adiciona uma nova guia Components às ferramentas de desenvolvedor. A nova guia de ferramentas de desenvolvedor pode ser usada para inspecionar os diferentes elementos React na aplicação, juntamente com seu estado e props:

captura de tela da extensão de ferramentas de desenvolvedor React

O estado do componente App é definido assim:

const [esquerda, setEsquerda] = useState(0)
const [direita, setDireita] = useState(0)
const [todosOsCliques, setTodos] = useState([])

As ferramentas do desenvolvedor mostram o estado dos hooks na ordem de sua definição:

estado dos hooks nas ferramentas do desenvolvedor React

O primeiro State (Estado) contém o valor do estado esquerda; o próximo contém o valor do estado direita e o último contém o valor do estado todosOsCliques.

Regras dos Hooks

Há algumas limitações e regras que devemos seguir para garantir que a nossa aplicação use corretamente as funções de estado baseadas em hooks.

A função useState ("usarEstado", assim como a função useEffect, ou "usarEfeito", introduzida mais tarde neste curso) não deve ser chamada dentro de um loop, uma expressão condicional ou qualquer lugar que não seja uma função que define um componente. Assim deve ser para garantir que os hooks sejam sempre chamados na mesma ordem e, se isso não acontecer, a aplicação se apresentará erros.

Resumindo, hooks só podem ser chamados de dentro do corpo de uma função que define um componente React:

const App = () => {
  // Desta forma funciona!
  const [idade, setIdade] = useState(0)
  const [nome, setNome] = useState('Juha Tauriainen')

  if ( idade > 10 ) {
    // Desta forma não funciona!
    const [foobar, setFoobar] = useState(null)
  }

  for ( let i = 0; i < idade; i++ ) {
    // Não faça deste jeito também!
    const [formaCorreta, setFormaCorreta] = useState(false)
  }

  const bemRuim = () => {
    // Isso também não é permitido!
    const [x, setX] = useState(-1000)
  }

  return (
    //...
  )
}

Revisão sobre Gerenciamento de Eventos (Event Handling)

O gerenciamento de eventos se mostrou um tópico difícil em iterações anteriores neste curso.

Por essa razão, revisaremos o tópico.

Vamos supor que estejamos desenvolvendo essa aplicação simples com o seguinte componente App:

const App = () => {
  const [valor, setValor] = useState(10)

  return (
    <div>
      {valor}
      <button>zerar</button>
    </div>
  )
}

Queremos que o clique do botão reinicialize o estado armazenado na variável valor.

Para fazer com que o botão reaja a um evento de clique, precisamos adicionar um gerenciador de evento a ele.

Os gerenciadores de eventos devem sempre ser uma função ou uma referência a uma função. O botão não funcionará se o gerenciador de evento for definido como uma variável de outro tipo.

Se definíssemos o gerenciador de evento como uma string:

<button onClick="lixo...">botão</button>

o React nos avisaria sobre isso no console:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `string` type.
    in button (at index.js:20)
    in div (at index.js:18)
    in App (at index.js:27)

A mensagem de erro diz: index.js:2178 Aviso: Esperava-se que o ouvinte onClick fosse uma função, mas obteve-se um valor do tipo string.

O seguinte também não funcionaria:

<button onClick={valor + 1}>botão</button>

Tentamos definir o gerenciador de evento como valor + 1, o que simplesmente retorna o resultado da operação. React nos avisará sobre isso no console:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `number` type.

A mensagem de erro diz: index.js:2178 Aviso: Esperava-se que o ouvinte onClick fosse uma função, mas obteve-se um valor do tipo number.

Este também não funcionaria:

<button onClick={valor = 0}>botão</button>

O gerenciador de evento não é uma função, mas uma atribuição de variável, e React, mais uma vez, emitirá um aviso no console. Esta tentativa também é falha no sentido de que nunca devemos mudar diretamente o estado em React.

Vejamos o próximo exemplo:

<button onClick={console.log('clicou no botão')}>
  botão
</button>

A mensagem é impressa no console assim que o componente é renderizado, mas nada acontece quando clicamos no botão. Por que não funciona mesmo quando nosso gerenciador de evento contém a função console.log?

O problema aqui é que nosso gerenciador de evento é definido como uma chamada de função, o que significa que o gerenciador de evento é atribuído ao valor retornado da função, que no caso de console.log é undefined (indefinido).

A função console.log é chamada quando o componente é renderizado e, por esse motivo, é impresso uma vez no console.

A tentativa a seguir também não funciona:

<button onClick={setValue(0)}>botão</button>

Novamente, tentamos definir uma chamada de função como o gerenciador de evento. Isso não funciona. Essa tentativa específica também causa outro problema: quando o componente é renderizado, a função setValue(0) é executada, o que por sua vez faz com que o componente seja renderizado novamente. A re-renderização, por conseguinte, chama setValue(0) novamente, resultando em uma recursão infinita.

A execução de uma chamada de função específica quando o botão é clicado pode ser realizada da seguinte maneira:

<button onClick={() => console.log('clicou no botão')}>
  botão
</button>

Agora, o gerenciador de evento é uma função definida com a sintaxe de uma arrow function, isto é, () => console.log('clicou no botão'). Quando o componente é renderizado, nenhuma função é chamada e apenas a referência à arrow function é definida como o gerenciador de evento. A chamada da função ocorre apenas quando o botão é clicado.

Podemos implementar a reinicialização do estado em nossa aplicação com essa mesma técnica:

<button onClick={() => setValue(0)}>botão</button>

O gerenciador de evento agora é a função () => setValue(0).

Definir gerenciadores de eventos diretamente no atributo do botão nem sempre é a melhor opção a se aplicar.

Você verá frequentemente gerenciadores de eventos definidos em um lugar separado. Na versão seguinte de nossa aplicação, definimos uma função que então é atribuída à variável handleClique no corpo da função do componente:

const App = () => {
  const [valor, setValor] = useState(10)

  const handleClique = () =>
    console.log('clicou no botão')

  return (
    <div>
      {valor}
      <button onClick={handleClique}>botão</button>
    </div>
  )
}

Agora, a variável handleClique está atribuída a uma referência à função. A referência é passada ao botão como o atributo onClick:

<button onClick={handleClique}>botão</button>

Naturalmente, nossa função gerenciadora de eventos pode ser composta por múltiplos comandos. Nestes casos, usamos a sintaxe de chaves mais longa para arrow functions:

const App = () => {
  const [valor, setValor] = useState(10)

  const handleClique = () => {    console.log('clicou no botão')    setValor(0)  }
  return (
    <div>
      {valor}
      <button onClick={handleClique}>botão</button>
    </div>
  )
}

Uma função que retorna outra função

Outra maneira de definir um gerenciador de evento é usar uma função que retorna outra função.

Provavelmente, você não precisará usar funções que retornam funções em nenhum dos exercícios deste curso. Se o tópico parecer confuso demais, você pode pular esta seção por enquanto e retornar a ela mais tarde.

Vamos fazer as seguintes alterações em nosso código:

const App = () => {
  const [valor, setValor] = useState(10)

  const ola = () => {    const gerenciador = () => console.log('Olá, mundo!')    return gerenciador  }
  return (
    <div>
      {valor}
      <button onClick={ola()}>botão</button>
    </div>
  )
}

O código funciona corretamente, apesar de parecer complicado.

O gerenciador de evento agora está definido como uma chamada de função:

<button onClick={ola()}>botão</button>

Anteriormente, afirmamos que um gerenciador de evento não pode ser uma chamada de função e que precisa ser ou uma função ou uma referência a uma função. Então, por que uma chamada de função funciona neste caso?

Quando o componente é renderizado, a seguinte função é executada:

const ola = () => {
  const gerenciador = () => console.log('Olá, mundo!')

  return gerenciador
}

O valor de retorno da função é outra função que é atribuída à variável gerenciador.

Quando o React renderiza a linha:

<button onClick={ola()}>botão</button>

Ele atribui o valor de retorno de ola() ao atributo onClick. Essencialmente, a linha se transforma em:

<button onClick={() => console.log('Olá, mundo!')}>
  botão
</button>

Como a função ola retorna uma função, o gerenciador de evento passa, agora, a ser uma função.

Qual é o objetivo deste conceito?

Vamos mudar um pouco o código:

const App = () => {
  const [valor, setValor] = useState(10)

  const ola = (quem) => {    const gerenciador = () => {      console.log('Olá', quem)    }    return gerenciador  }
  return (
    <div>
      {valor}
      <button onClick={ola('mundo')}>botão</button>      <button onClick={ola('react')}>botão</button>      <button onClick={ola('função')}>botão</button>    </div>
  )
}

Agora, a aplicação têm três botões com gerenciadores de eventos definidos pela função ola que aceita um único parâmetro.

O primeiro botão é definido como:

<button onClick={ola('mundo')}>botão</button>

O gerenciador de evento é criado executando a chamada da função ola('mundo'). A chamada da função retorna a função:

() => {
  console.log('Olá', 'mundo')
}

O segundo botão é definido como:

<button onClick={ola('react')}>botão</button>

A chamada da função ola('react') que cria o gerenciador de evento retorna:

() => {
  console.log('Olá', 'react')
}

Ambos os botões obtêm seus gerenciadores de eventos individualizados.

Funções que retornam funções podem ser utilizadas na definição de funcionalidades genéricas que podem ser personalizadas com parâmetros. A função ola que cria os gerenciadores de eventos pode ser analisada como uma fábrica que produz gerenciadores de eventos personalizados destinados a saudar usuários.

Nossa definição atual é um tanto verbosa:

const ola = (quem) => {
  const gerenciador = () => {
    console.log('Olá', quem)
  }

  return gerenciador
}

Vamos excluir as variáveis de ajuda e retornar diretamente a função criada:

const ola = (quem) => {
  return () => {
    console.log('Olá', quem)
  }
}

Por conta de nossa função ola ser composta por um único comando de retorno, podemos omitir as chaves e usar a sintaxe mais compacta para funções de seta:

const ola = (quem) =>
  () => {
    console.log('Olá', quem)
  }

Por fim, vamos escrever todas as setas na mesma linha:

const ola = (quem) => () => {
  console.log('Olá', quem)
}

Podemos usar o mesmo "macete" para definir gerenciadores de eventos que definem o estado do componente para um determinado valor. Vamos fazer as seguintes alterações em nosso código:

const App = () => {
  const [valor, setValor] = useState(10)
  
  const setNoValor = (novoValor) => () => {    console.log('setValor atual', novoValor)  // Imprime o novo valor no console    setValor(novoValor)  }  
  return (
    <div>
      {valor}
      <button onClick={setNoValor(1000)}>mil</button>      <button onClick={setNoValor(0)}>zerar</button>      <button onClick={setNoValor(valor + 1)}>incrementar</button>    </div>
  )
}

Quando o componente é renderizado, é criado o botão mil:

<button onClick={setNoValor(1000)}>mil</button>

O gerenciador de evento é definido como o valor retornado de setNoValor(1000), que é a seguinte função:

() => {
  console.log('setValor atual', 1000)
  setValor(1000)
}

O botão de incremento é declarado da seguinte forma:

<button onClick={setNoValor(valor + 1)}>incrementar</button>

O gerenciador de evento é criado pela chamada da função setNoValor(valor + 1), que recebe como parâmetro o valor atual da variável de estado valor incrementado em 1 (um). Se o conteúdo de valor fosse 10, então o gerenciador de evento criado seria a seguinte função:

() => {
  console.log('setValor atual', 11)
  setValor(11)
}

Não é necessário usar funções que retornam funções para alcançar esta funcionalidade. Vamos retornar a função setNoValor, responsável por atualizar o estado, como uma função normal:

const App = () => {
  const [valor, setValor] = useState(10)

  const setNoValor = (novoValor) => {
    console.log('setValor atual', novoValor)
    setValor(novoValor)
  }

  return (
    <div>
      {valor}
      <button onClick={() => setNoValor(1000)}>
        mil
      </button>
      <button onClick={() => setNoValor(0)}>
        zerar
      </button>
      <button onClick={() => setNoValor(valor + 1)}>
        incrementar
      </button>
    </div>
  )
}

Agora, podemos definir o gerenciador de evento como uma função que chama a função setNoValor com um parâmetro apropriado. O gerenciador de evento utilizado para redefinir o estado da aplicação seria:

<button onClick={() => setNoValor(0)}>zerar</button>

Escolher entre as duas formas apresentadas de definir seus gerenciadores de eventos é, em grande parte, uma questão de gosto.

Passando Gerenciadores de Evento para Componentes-filho

Vamos extrair o botão para seu próprio componente:

const Botao = (props) => (
  <button onClick={props.handleClique}>
    {props.texto}
  </button>
)

O componente obtém a função de gerência de evento da propriedade handleClique, e o texto do botão da propriedade texto. Vamos usar o novo componente:

const App = (props) => {
  // ...
  return (
    <div>
      {valor}
      <Botao handleClique={setNoValor(1000)} texto="mil" />      <Botao handleClique={setNoValor(0)} texto="zerar" />      <Botao handleClique={setNoValor(valor + 1)} texto="incrementar" />    </div>
  )
}

Usar o componente Botao é simples, embora tenhamos que nos certificar de usar os nomes corretos de atributo ao passar props para o componente.

captura de tela do código de nomes de atributos corretos Nota dos tradutores: ao longo do texto, apresentamos os códigos contendo termos traduzidos para o português, os quais não aparecem na imagem acima, pois esta traz o código escrito com os termos em inglês.

Não defina Componentes dentro de Componentes

Vamos começar a exibir o valor da aplicação em seu componente Exibir.

Vamos mudar a aplicação definindo um novo componente dentro do componente App.

// Este é o lugar correto para definir um componente
const Botao = (props) => (
  <button onClick={props.handleClique}>
    {props.texto}
  </button>
)

const App = () => {
  const [valor, setValor] = useState(10)

  const setNoValor = novoValor => {
    console.log('setValor atual', novoValor)
    setValor(novoValor)
  }

  // Não defina um componente dentro de outro componente
  const Exibir = props => <div>{props.valor}</div>
  return (
    <div>
      <Exibir valor={valor} />
      <Botao handleClique={() => setNoValor(1000)} texto="mil" />
      <Botao handleClique={() => setNoValor(0)} texto="zerar" />
      <Botao handleClique={() => setNoValor(valor + 1)} texto="incrementar" />
    </div>
  )
}

A aplicação ainda parece funcionar, porém, não implemente componentes desta forma! Nunca defina componentes dentro de outros componentes. O método não oferece nenhum benefício e leva a muitos problemas desagradáveis. Os maiores problemas acontecem devido ao React tratar um componente definido dentro de outro componente como um novo componente em cada renderização. Isso torna impossível para o React otimizar o componente.

Em vez disso, vamos mover a função do componente Exibir para o seu lugar correto, que fica fora da função do componente App:

const Exibir = props => <div>{props.valor}</div>

const Botao = (props) => (
  <button onClick={props.handleClique}>
    {props.texto}
  </button>
)

const App = () => {
  const [valor, setValor] = useState(10)

  const setNoValor = novoValor => {
    console.log('setValor atual', novoValor)
    setValor(novoValor)
  }

  return (
    <div>
      <Exibir valor={valor} />
      <Botao handleClique={() => setNoValor(1000)} texto="mil" />
      <Botao handleClique={() => setNoValor(0)} texto="zerar" />
      <Botao handleClique={() => setNoValor(valor + 1)} texto="incrementar" />
    </div>
  )
}

Leitura Recomendada

A internet está cheia de material relacionado à biblioteca React. No entanto, usamos o novo estilo de programação em React para o qual a grande maioria do material encontrado online está desatualizado.

Estes links talvez possam lhe ser úteis:

  • Vale a pena dar uma olhada em algum momento na documentação oficial React, embora a maior parte dela só se torne relevante mais para frente no curso. Além disso, tudo relacionado a componentes baseados em classe é irrelevante para nós;
  • Alguns cursos no Egghead.io, como o Start learning React, são de altíssima qualidade; e o recentemente atualizado Beginner's Guide to React também é relativamente bom; ambos os cursos introduzem conceitos que também serão introduzido no decorrer deste curso. Obs.: O primeiro curso usa componentes de classe, mas o segundo usa a nova abordagem baseada em funções.

Juramento do Programador Web

Programar é difícil, e é por isso que eu usarei todos os meios possíveis para ser mais fácil:

  • Eu manterei meu Console do navegador aberto o tempo todo;
  • Eu vou progredir aos poucos, passo a passo;
  • Eu escreverei muitas instruções console.log para ter certeza de que estou entendendo como o código se comporta e para me ajudar a identificar os erros;
  • Se meu código não funcionar, não escreverei mais nenhuma linha no código. Em vez disso, começarei a excluir o código até que funcione ou retornarei ao estado em que tudo ainda estava funcionando; e
  • Quando eu pedir ajuda no canal do Discord do curso ou em outro lugar, formularei minhas perguntas de forma adequada. Veja aqui como pedir ajuda.