Habemus React Hooks

Uma das features mais interessantes do React foi finalmente lançada, o famoso Hooks! Por que é tão legal assim?

Introdução

Faaaala pessoal! A ideia era ter escrito esse post bem no dia do release dos hooks, mas acabou que tive uns imprevistos, mas antes tarde do que nunca né?

Esse post vai beber basicamente da fonte da documentação do React, vou tentar condensar alguns detalhes e fazer outros comentários, se você já leu tudo lá e já entendeu, talvez o post seja repetitivo, mas não vá embora ainda não, veja que belo gatinho.

Gatinho fofinho olhando com cara de pidão

Bom, enquanto escrevo esse post, vou ouvindo a trilha sonora de um jogo incrível chamado Gris, é composta basicamente por pianos, ótima para concentrar e para quem curte jogos, aconselho demais a dar uma conferida.

O que é? Onde vive? O que come?

Assim como quando algo novo lança, vários devem estar se perguntando para que servem os tais hooks? Qual o ganho nisso? Vou ter que aprender tudo de novo?

Enfim, são váaarias dúvidas e aí eu separei essa parte inicial para responder algumas delas. Se você quiser ver ainda mais perguntas/respostas, o pessoal do React fez um excelente FAQ.

O que são React Hooks?

Numa versão bem resumida:

Hooks permitem que você utilize states e outros métodos de states sem precisar criar uma classe. Você também pode criar seus próprios Hooks e compartilhar a lógica entre mais componentes.

Ou seja, aqueles métodos como componentDidMount e componentDidUpdate que as vezes se tornavam complexos em componentes maiores, agora poderão ser simplificados na nova lógica dos hooks, além de poderem ser compartilhados.

Por que criaram isso?

O Dan Abramov fez uma palestra no ano passado explicando todos os conceitos e motivações para criar essa nova estrutura, eu acho que vale super a pena assistir:

Uma outra fonte que eu acho super legal é esse Tweet abaixo:

Se você perceber, o código não ficou muito menor, mas as responsabilidades ficaram mais organizadas em seus devidos blocos, o que já facilita DEMAIS na escrita e leitura do código.

Existem outros vários motivos, mas vou pontuar os principais aqui:

É difícil de reutilizar lógica de estados entre componentes

A forma mais comum de compartilharmos comportamentos/funcionamentos entre componentes era através dos HOC's (Higher-order components) e das render props.

O grande problema desses padrões é que você precisa modificar boa parte do código do componente para que o mesmo se adapte ao funcionamento compartilhado, aumentando sua verbosidade e perdendo boa parte do isolamento de responsabilidade, ou seja, você acaba perdendo qual parte do código faz o que.

Classes ainda confundem pessoas e máquinas

Quando eu escrevi o post Fundamentos JavaScript antes de aprender React, um dos primeiros conceitos que abordei lá, foi exatamente o uso de classes.

A equipe do React notou que o uso de classes ainda é uma grande barreira para aprender React. Você precisa entender como o Javascript funciona, que é bem diferente da maioria das linguagens orientadas a objeto. Você precisa lembrar de fazer o bind dos eventos e também entender qual é o this para cada contexto, o que pode ser simples para uns, mas ainda muito complicado para muitos.

Adicionalmente, o React é uma biblioteca que já está no mercado há aproximadamente 5 anos, mas eles querem que ela continue relevante por mais e mais anos. Para isso, eles já estão se preocupando com outras otimizações e eles notaram que o uso de classes pode permitir o uso de certas patterns que prejudicariam essa otimização, como uma não tão boa minificação e outros detalhes de mais baixo nível.

Para resolver esses problemas, os Hooks permitem que você utilize todas as features do React mas sem a necessidade de utilizar classes. Os componentes React sempre foram mais ligados a funções e os hooks vem para tornar isso ainda mais comum.

Preciso aprender tudo de novo?

A resposta curta e grossa é não. Os Hooks são totalmente opcionais e você pode criar componentes novos utilizando essa nova estrutura e utilizar lado a lado com componentes antigos, tudo vai funcionar sem problemas. Mas se você não quiser ou não tiver tempo de ler sobre hooks, não há problema nenhum, você pode continuar a vida como está.

Hooks não adicionam nada novo nos conceitos de React. Eles somente adicionam uma forma mais direta de mexer na API do react, como: props, state, context, refs, e lifecycle.

Eles inclusive encorajam que você não saia reescrevendo tudo do zero, mas que vá gradualmente adotando os hooks.

Hooks e seus funcionamentos

Existem alguns hooks e inclusive você pode criar os seus próprios! Eu vou mostrar inicialmente os mais importantes, fazendo comparações de antes e depois, para ajudar a entender o funcionamento.

Usando o Hook de estados (useState)

Seguindo a ideia de antes/depois, segue abaixo um Componente Class Based:

class Example extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    )
  }
}

Utilizando hooks, ficaria assim:

import React, { useState } from 'react'

function Example() {
  // Declare a new state variable, which we'll call "count"
  // Declarando uma nova variável de estado, que chamamos de "count"
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}

Basicamente, a grande mudança fica por conta desse useState que é importado diretamente do pacote do react. Temos também ali a criação do count e também um método setCount e por último, temos o método setCount sendo chamado no onClick assim como o this.setState era chamado antes. Bom, vamos quebrar isso em pedacinhos para entender.

Declarando a variável de estado

Numa classe, nós inicializamos o count como 0 definindo através do this.state lá no construtor.

...
constructor(props) {
  super(props);
  this.state = {
    count: 0
  };
}
...

Num function component ou antes chamado também de stateless component (antes não controlávamos estados em funções puras), nós não tínhamos o this.state para poder definir valores ou chamar valores. Mas agora, com o hooks, nós podemos chamar o useState diretamente do nosso componente:

const [count, setCount] = useState(0)
  • E o que o useState faz?

Ele declara uma "state variable". Essa variável é chamada count para esse exemplo, mas poderia se chamar qualquer coisa, como fruit. Essa é a maneira de "preservar" os valores entre as funções. O useState tem as mesmas habilidades que o this.state tem para a classe. Normalmente variáveis desaparecem depois que a função é executada, mas os estados são preservados no React e isso é o que vai ocorrer com as variáveis criadas pelo useState.

  • O que nós passamos de argumento no useState?

O useState aceita somente um argumento e ele é o estado inicial da variável. Diferente das classes, o estado não precisa ser um objeto nesse caso. Ele pode ser somente um número ou uma string, se é tudo que precisamos. No nosso exemplo, estamos alterando somente a quantidade de vezes que o usuário está clicando, então 0 é mais que suficiente.

Importante: se precisarmos guardar dois diferentes valores no estado, iremos utilizar o useState duas vezes.

  • O que o useState retorna?

Esse método retorna um par de valores: o estado atual e uma função que atualiza o mesmo. E é por isso que escrevemos [count, setCount] = useState().

Essa forma de assinalar 2 valores ao mesmo tempo é utilizando o destructuring, que é uma feature que veio no ES6, não é exclusiva do React.

Agora que sabemos como o useState funciona, as coisas vão fazer mais sentido.

const [count, setCount] = useState(0)

Ali nós declaramos a variável count, que vai ter 0 como seu valor inicial e criamos o método setCount que vai ser responsável por fazer a atualização do count.

Lendo a variável de estado

Para ler o valor da variável, na classe nós utilizávamos assim:

<p>You clicked {this.state.count} times</p>

Ou seja, precisávamos buscar no nosso objeto this.state por cada variável. Nas funções com hooks podemos chamar a variável diretamente:

<p>You clicked {count} times</p>

Atualizando a variável de estado

Na classe, nós precisávamos do método this.setState() para atualizar os valores:

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
  Click me
</button>

Na função com hooks, nós já definimos tanto o setCount quanto o count, então fica bem mais simples:

<button onClick={() => setCount(count + 1)}>Click me</button>

Se você ainda tem alguma dúvida nessa parte, recomendo ir nessa parte da documentação, onde tem mais alguns detalhes.

Usando o Hook de efeitos (useEffect)

Vimos acima como trabalhar com o useState para definir nossos estados, mas e nos casos que precisamos encadear ações baseadas em outras mudanças, como fazíamos no componentDidMount, componentDidUpdate e componentWillUnmount?

Seguindo a mesma ideia de antes/depois, segue um exemplo onde atualizamos o title da página baseada no nosso count. Primeiro class based:

class Example extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    )
  }
}

Repare que temos código duplicado ali! Isso acontece pois precisamos realizar a operação 2x, primeiro quando o componente é montado na página componentDidMount e depois quando ele é atualizado componentDidUpdate.

Agora vejamos com o useEffect hook:

import React, { useState, useEffect } from 'react'

function Example() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    document.title = `You clicked ${count} times`
  })

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  )
}
  • O que o useEffect faz?

Usando esse Hook, você diz ao React que o componente precisa fazer algo depois de renderizar. Desta forma, no momento que o componente renderizar, o React vai chamar o método e toda vez que atualizarmos, ele também irá chamar o método. Nós utilizamos para uma simples mudança de document.title, mas poderia ser utilizado para um fetch numa API, por exemplo.

  • Por que o useEffect é chamado dentro do componente?

Colocando o useEffect dentro do componente nos permite acessar a variável de count ou qualquer props que precisarmos. Tendo já dentro da função, não precisamos de nenhuma API para ler, já está dentro do escopo da função.

  • O useEffect roda toda vez que renderiza?

Sim, por padrão ele vai rodar logo após ser renderizado e toda vez que for atualizado. Mais para frente veremos que também podemos customizar isso.

Efeitos com Cleanup

No exemplo acima, nós estamos alterando somente um ponto e não estamos "vigiando" nenhuma mudança em nenhum outro canto. Mas em algumas ocasiões nossos componentes precisam "vigiar" eventos enquanto estiverem na tela e depois precisamos limpar isso, para não correr o risco de ter memory leak e travar toda a aplicação. Usando classes, nós utilizamos o componentWillUnmount exatamente para fazer essa limpeza. Abaixo segue um exemplo onde trabalhamos com um módulo chamado ChatAPI:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props)
    this.state = { isOnline: null }
    this.handleStatusChange = this.handleStatusChange.bind(this)
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    )
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    )
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    })
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...'
    }
    return this.state.isOnline ? 'Online' : 'Offline'
  }
}

Repare que ao montar o componente nós fazemos um subscribeToFriendStatus e ao desmontar, fazemos exatamente o oposto com unsubscribeFromFriendStatus.

Já utilizando o useEffect hook faremos assim:

import React, { useState, useEffect } from 'react'

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null)

  function handleStatusChange(status) {
    setIsOnline(status.isOnline)
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
    }
  })

  if (isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}

Dentro do nosso método useEffect, nós faremos o retorno de uma função, nesse caso a função cleanup (não necessariamente precisa ter esse nome ou sequer ser uma named function).

O React ao ver que o useEffect possui uma função como retorno, automaticamente irá rodar sempre que o componente for desmontado. Dessa forma, nós temos o funcionamento de componentDidMount e componentDidUnmount como na classe.

Dentro da documentação existe uma parte com algumas dicas para o useEffect, devo fazer um post futuramente com essa parte em separado, mas vale dar uma olhada lá também.

Regras para os Hooks

Como já vimos, os Hooks são funções JavaScript, mas é necessário seguir duas regras fundamentais para poder usá-los. Inclusive existe um plugin para o eslint que ajuda a não esquecer dessas regras.

Somente chame os Hooks no Top Level

Não chame Hooks dentro de loops, condicionais ou funções aninhadas. Ao invés disso, sempre chame os Hooks na primeira camada da sua função React. Seguindo esta regra, você garante que os Hooks serão chamados na mesma ordem. Isso permite que o React preserve corretamente o estado dos Hooks quando usados múltiplos useState e useEffect.

Somente chame os Hooks em funções React

Dessa forma você garante que toda a lógica de estados será visível por todo o componente.

Explicação

Um mesmo componente pode ter múltiplos useState e useEffect. Como o exemplo abaixo:

function Form() {
  // 1. Use a variável "name" no state
  const [name, setName] = useState('Mary')

  // 2. Use um effect para persistir os dados do form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name)
  })

  // 3. Use a variável "surname" no state
  const [surname, setSurname] = useState('Poppins')

  // 4. Use um effect para atualizar o título da página
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname
  })

  // ...
}

Então, como o React sabe qual estado corresponde a qual chamada do useState? A resposta é o React depende da ordem com que os Hooks são chamados. O exemplo acima funciona, pois a ordem dos Hooks é sempre a mesma em todas as renderizações. Segue abaixo um fluxo:

// ------------
// Primeira renderização
// ------------
useState('Mary') // 1. Inicializa a variável "name" como 'Mary'
useEffect(persistForm) // 2. Adiciona um effect para persistir o form
useState('Poppins') // 3. Inicializa a variável "surname" com 'Poppins'
useEffect(updateTitle) // 4. Adiciona um effect para atualizar o título

// -------------
// Segunda renderização
// -------------
useState('Mary') // 1. Lê a variável "name" (o argumento é ignorado)
useEffect(persistForm) // 2. Recoloca o efeito para persistir o form
useState('Poppins') // 3. Lê a variável "surname" (o argumento é ignorado)
useEffect(updateTitle) // 4. Recoloca o efeito para atualizar o título
// ...

Enquanto a ordem permanecer a mesma, não há problema nenhum, mas o que acontece se colocar uma condicional para um dos hooks?

// 🔴 Estamos quebrando a primeira regra!
if (name !== '') {
  useEffect(function persistForm() {
    localStorage.setItem('formData', name)
  })
}

A condição name !== '' é verdadeira na primeira renderização, então chamamos o Hook. Entretanto, na próxima renderização o usuário pode querer limpar o form, fazendo a condição ser false. E com isso, a ordem de execução do hook muda:

useState('Mary') // 1. Lê a variável "name" (o argumento é ignorado)
// useEffect(persistForm)  // 🔴 Esse hook foi passado
useState('Poppins') // 🔴 2 (mas era 3). Falha para ler o "surname"
useEffect(updateTitle) // 🔴 3 (mas era 4). Falha para substituir o effect

O React não saberia o que devolver para a segunda chamada do useState. Ele esperava receber o useEffect para persistir o form, assim como feito na renderização anterior, com isso todas as chamadas iriam "pular" uma etapa e isso levaria a vários bugs de estado.

Sei que isso acima parece super complexo, para mim também soou, mas você não precisa se preocupar. Se você estiver usando o plugin para o eslint, ele nunca vai deixar você cometer esse erro e com isso você está salvo \o/

Criando seus próprios Hooks

Além dos hooks useState e useEffect que vimos, nós podemos criar nossos próprios hooks e compartilhar entre vários componentes, que é a parte mais interessante dos hooks.

Mais acima, nós tínhamos o seguinte componente, que servia para indicar se um amigo estava online ou offline.

import React, { useState, useEffect } from 'react'

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null)

  function handleStatusChange(status) {
    setIsOnline(status.isOnline)
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
    }
  })

  if (isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}

Agora, vamos dizer que na nossa aplicação também possuímos um contact list que renderiza os nomes dos usuários online com uma cor verde. Nós poderíamos copiar a lógica feito no FriendStatus e passaríamos para o nosso novo FriendListItem, mas isso não seria o ideal, olhe abaixo:

import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    <li style={ color: isOnline ? 'green' : 'black' }>
      {props.friend.name}
    </li>
  );
}

Repare que é praticamente tudo igual, a única diferença é na parte da renderização, mas os hooks são os mesmos. Para corrigir esse problema de duplicação, nós poderíamos ter 2 formas de compartilhar essa lógica, através do render props ou usando higher-order components.

Agora vamos ver como podemos solucionar esse problema utilizando hooks sem a necessidade de criar mais componentes.

Extraindo um Hook customizado

Quando queremos compartilhar lógica entre duas funções, nós extraímos em uma terceira função. Fazemos o mesmo com os hooks!

Um hook customizado é uma função JavaScript que começa com a palavra "use" e pode chamar outros hooks. Por exemplo, o useFriendStatus abaixo será nosso primeiro hook customizado.

import React, { useState, useEffect } from 'react'

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null)

  function handleStatusChange(status) {
    setIsOnline(status.isOnline)
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
    }
  })

  return isOnline
}

Repare que não há nada de novo nessa função, nós basicamente copiamos a lógica dos componentes acima.

Diferente de um componente React, um hook customizado não precisa de assinatura específica. Nós decidimos o que ele irá receber como argumentos e também o que iremos retornar se precisarmos retornar algo. Em outras palavras, é basicamente uma função normal em JavaScript, só precisa ser iniciado com a palavra use para seguir as regras de hooks mencionadas anteriormente.

Usando um hook customizado

Agora que já extraímos a lógica para um hook separado, podemos simplesmente utilizar o mesmo nos nossos componentes:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id)

  if (isOnline === null) {
    return 'Loading...'
  }
  return isOnline ? 'Online' : 'Offline'
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={ color: isOnline ? 'green' : 'black' }>
      {props.friend.name}
    </li>
  );
}

Repare que ficou bem mais limpo, sem código duplicado e sem a necessidade de criar um outro componente externo! Na documentação temos mais alguns detalhes também.

Nossa, o post ficou bem grande, mas tem muitos outros detalhes legais que valem a pena ver, então vou botar alguns links aqui:

Conclusão

Bom pessoal, espero que esse post tenha sido útil e que pelo menos te faça querer dar uma olhada a mais sobre hooks, acredito que essa foi uma enorme adição ao ecossistema React e será o futuro dessa lib. Ainda pretendo escrever mais posts sobre esse assunto, então fique atento! =)

Comentários