Press enter to see results or esc to cancel.

Simplificando componentes com React Hooks

Foi introduzido na versão 16.8 do React uma nova funcionalidade chamada Hooks, ela permite que você tenha acesso a recursos que até o momento exigiam que se fossem utilizadas classes, só que agora apenas com funções.

Motivação

Essa atualização veio para lidar com um problema comum no ambiente de desenvolvimento React, compartilhar lógica entre componentes. A comunidade acabou desenvolvendo alguns padrões para lidar com isso, como a utilização de componentes de alta ordem  e render props, que embora sejam técnicas funcionais, são consideradas avançadas e aumentam a complexidade de sua arquitetura de componentes com varias camadas de render.

Outro problema comum é que os componentes acabam se tornando complexos e difíceis de entender, com mistura de lógica de gerenciamento de estado e de eventos de componente. Normalmente cada método de ciclo de vida acaba cheio de lógicas que não se correlacionam. É comum por exemplo um componente consumir uma API no ComponentDidMount ou ComponentDidUpdate, e nesse mesmo ComponentDidMount também  definirmos event listeners que vão precisar ser limpos no ComponentWillUnmount, podemos considerar que buscar dados na API e definir event listeners não tem muita relação e estão em um mesmo lugar.

Em muitos casos não é possível dividir esses componentes em componentes menores por causa de toda lógica de estado e efeitos colaterais, alem de aumentar a dificuldade de testá-los. Por esses motivos é comum ver a utilização de outras bibliotecas para lidar com estado, tal como Redux, porém alem de acrescer a sua própria complexidade, também acaba por adicionar abstrações demais, nos forçando a criar e navegar por muitos arquivos e tornando menos viável de um componente ser reutilizável.

Outro motivador é que usar classes em JavaScript pode trazer algumas dores de cabeça, principalmente quando temos de lidar com o conceito de this, que não é parecido com o mesmo conceito em outras linguagens e também não é muito intuitivo, mesmo com uma proposta como a de class properties que ajudam a lidar com o problema de passar funções entre componentes de uma forma menos verborrágica que dar bind em cada função que será passada como parâmetro, ainda assim lidamos com o problema de não ser uma forma idiomática de JavaScript para escrever métodos, alem de ocasionar problemas de performance ou efeitos colaterais não esperados.

Como solução temos o React Hooks, que permite que você possa extrair toda lógica de gerenciamento de estado de seu componente tanto para testá-la quanto para reutilizá-la. Isso porque com ele você consegue quebrar toda essa lógica em funções com responsabilidades especificas.

Hook de Estado

O primeiro hook que iremos explorar é o de estado, aqui um exemplo de código:

import React, { useState } from 'react'

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

  return (
    
    <div>
       Você clicou {count} vezes!
      <button onClick={() => setCount(count + 1)}>
        Mais um!
      </button>
    </div>


  )
}

O hook nesse caso é o useState. Ele recebe o estado inicial, e retorna um array com dois valores (que estão sendo desconstruídos direto em variáveis) o primeiro valor é o estado atual e o segundo uma função para atualizar esse estado.  No onClick do botão chamamos a função de alterar o estado passando o estado atual mais um.

Vale ressaltar que o useState não funciona exatamente igual ao setState que utilizamos em classes. Quando passamos um objeto para o setState, ele combina o valor que estamos passando com o antigo. Enquanto no useState, todo o estado do hook será alterado, porém conseguimos o mesmo efeito usando o operador de spread do JavaScript, useState({ ...oldState, ...newState })

A primeira coisa a se pensar caso precisemos de um state mais complexo é ter um objeto com toda informação que precisamos,

Algo assim:

function Contador() {
  const [state, setState] = useState({ nome: '', idade: 0  })
  ...
  )
}

Só que acabamos ficando com uma função com nome genérico, e temos que lembrar de utilizar o operador de spread para combinar o estado antigo com o novo. Com hooks temos outra opção, podemos chamar mais de uma vez o nosso hook useState no corpo de nosso componente.

Desta forma:

function Contador() {
  const [nome, setNome] = useState('')
  const [idade, setIdade] = useState(0)
  ...
  )
}

Assim temos funções separadas e explicitas de como consumir ou alterar o estado do componente.

Alguns pontos de atenção sobre o uso de hooks, a ordem importa, então eles NÃO devem ser chamados em condicionais ou dentro de laços, sendo assim eles devem ficar sempre no topo do corpo da função de seu componente, alguns linters foram criados pelo time do React para te lembrar disso.
Alem disso hooks não devem ser utilizados em classes ou fora de qualquer função no contexto do React, basicamente componentes e custom hooks(que vamos falar mais pra frente).

Hook de Effects

Outro hook importante criado pelo time do React é o useEffect, ele permite que seu componente em forma de função tenha acesso aos métodos de ciclo de vida sem precisar refatorar seu componente para uma classe.

Exemplo:


import React, { useState, useEffect } from 'react'
 
function Contador() {
  const [count, setCount] = useState(0)
 
  useEffect(() => {
    document.title = `Você clicou ${count} vezes!`
  })

  return (
    <div>
      Você clicou {count} vezes!
      <button onClick={() => setCount(count + 1)}>
        Mais um!
      </button>
    </div>
)}

Nesse caso o titulo da pagina será alterado de acordo com atualização do estado do componente, na pratica o useEffect nesse contexto é equivalente ao ComponentDidMount e também ao ComponentDidUpdate. Ele irá invocar a função passada tanto quando o componente é montado quando é atualizado.

O useEffect também te da uma forma de fazer a limpeza de recursos, exatamente o que você usaria no ComponentWillUnmount. Para isso basta retornar uma função de limpeza.

Exemplo:

function Canvas() {
  const [x, setX] = useState(0)
  const [y, setY] = useState(0)
 
  useEffect(() => {
      const mouseMove = e => {
        setX(e.screenX)
        setY(e.screenY)
      }

      document.addEventListener('mousemove', mouseMove)
      return () => document.removeEventListener('mousemove', mouseMove)
  })

  return (
    <div>
        Mouse esta no {x}, {y}
    </div>
)}

Nesse exemplo, ao montar o componente temos o evento de mousemove configurado para alterar o estado do componente de acordo com o movimento do mouse e quando o componente for desmontado será rodado o removeEventListener, porém essa função de limpeza também será chamada quando for detectado que o useEffect precisa rodar novamente, ou seja em cada render.
Por causa disso a cada alteração no estado do componente nosso evento está sendo removido e adicionado novamente. Em alguns casos pode ser algo que queremos, porém agora não. O que precisamos é que o evento seja adicionado na montagem apenas e a limpeza na desmontagem.

Para isso vamos utilizar o segundo argumento que o useEffect recebe, que é uma lista dos valores que devem mudar para que ele rode novamente. Se passarmos uma lista vazia, ele irá rodar apenas quando é montado e a função de limpeza apenas quando é desmontado.

function Canvas() {
  const [x, setX] = useState(0)
  const [y, setY] = useState(0)
 
  useEffect(() => {
      const mouseMove = e => {
        setX(e.clientX)
        setY(e.clientY)
      }

      document.addEventListener('mousemove', mouseMove)
      return () => document.removeEventListener('mousemove', mouseMove)
  }, []) // <-- aqui a lista vazia

  return (
    <div>
        Mouse esta no {x}, {y}
    </div>
)}

Dessa forma nossos event listeners serão chamados apenas quando precisamos.
Podemos utilizar esse segundo parâmetro para dizer quando nosso efeito vai rodar, por exemplo, em um caso de chat, queremos nos inscrever em um stream de mensagens para um cliente e caso o cliente mude precisamos cancelar a inscrição do atual e nos inscrever novamente.

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

  useEffect(() => {
    const handleStatusChange = status => setIsOnline(status.isOnline)

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)

    return () => ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)

  }, [props.friend.id]) // apenas se desinscreve caso props.friend.id mude
}

Toda vez que o friend.id mudar, iremos chamar o unsubscribeFromFriendStatus com id anterior e depois chamar o subscribeToFriendStatus com id atual, assim temos consistência na limpeza dos recursos de forma simples. Exatamente por esse motivo que a API do useEffect foi pensada, para ter ciclos de vida simples, e não precisar de vários hooks diferentes. Em comparação com uma função para cada método de ciclo de vida como é quando utilizamos classes.

Outros hooks…

Vários hooks já vem por padrão na biblioteca do React. Mas vale abordar brevemente dois deles. O useContext, que te dá acesso direto à API de contexto do react e permite que passemos dados de um componentes para seus filhos sem ter que declarar as props de forma explícita.

function Example() {
  const locale = useContext(LocaleContext)
  const theme = useContext(ThemeContext)
  // ...
}

Outro hook interessante é o useReducer, que parece um pouco o useState, porém ele não te devolve uma função para alterar o estado, e sim uma função de dispatch, que envia uma mensagem de como o estado deve ser alterado. Para isso precisamos definir uma função de reducer, que é responsável por receber a mensagem e alterar o estado atual.

const estadoInicial = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'incrementar':
      return { count: state.count + 1 }
    case 'decrementar':
      return { count: state.count - 1 }
    default:
      return state
  }
}

function Counter({ estadoInicial }) {
  const [state, dispatch] = useReducer(reducer, estadoInicial );
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'incrementar'})}>+</button>
      <button onClick={() => dispatch({type: 'decrementar'})}>-</button>
    </>
  );
}

Nesse exemplo o dispatch ira chamar a função de reducer toda vez que um botão for clicado, passando como parâmetro a ação que ele deve fazer. A função de reducer por sua vez, recebe o estado atual e a mensagem de ação, e a partir disso sabe gerar um novo estado.
Esta opção de hook é interessante em casos mais complexos de gerenciamento de estado.

Criando nossos hooks!

A parte mais legal dos hooks é o fato deles serem totalmente desacoplados de componentes, o que nos permite combiná-los para criar novos hooks mais específicos e compartilhar lógica entre nossos componentes.

Vamos imaginar um componente no contexto de chat para lidar com status de usuário:

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

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

  useEffect(() => {
    const handleStatusChange = status => status.isOnline
    
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)

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

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

Só que vamos imaginar que além disso, iremos precisar de uma forma de lidar com uma lista de contatos e exibir seus respectivos status.

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

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

  useEffect(() => {
    const handleStatusChange = status => status.isOnline

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

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

Claramente temos uma repetição de código aqui. Toda lógica de gerenciamento de estado é idêntica. Para resolver isso podemos extrair a lógica repetida em um hook customizado

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

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

  useEffect(() => {
    const handleStatusChange = status => status.isOnline

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

  return isOnline
}

Não tem nada de novo aqui, é apenas a lógica que tínhamos em nossos componentes, só que agora em uma função separada (é padrão que todo hook tenha o sufixo use). Com isso podemos utilizá-lo diretamente em nossos componentes previamente criados:

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>
  )
}

Sendo assim conseguimos compartilhar a lógica e simplificar nossos componentes.

Também Podemos criar hooks para lidar com bibliotecas externas tal como RxJs, criando uma forma simples de combinar React com a biblioteca mais famosa de programação reativa:

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

const useObservable = (observable, initialValue) => {
  const [value, setValue] = useState(initialValue)
  useEffect(() => {
    const subscription = observable.subscribe({next: setValue})
    return () => subscription.unsubscribe()
  }, [observable])
  return value
}

Nesse código a cada novo evento no stream do observable temos uma atualização no estado, e o gerenciamento de subscription quase de graça.

O uso do nosso hook ficaria desta forma:

import React from 'react'
import { fromEvent } from 'rxjs'
import { map }  from 'rxjs/operators'
import { useObservable } from './observableHook'


const mouse$ = fromEvent(document, 'mousemove').pipe(
  map(e => [e.clientX, e.clientY])
)

const App = () => {
   const [x,y] = useObservable(mouse$, [0,0])

   return (
     <div>Mouse x:{x} y:{y}</div>
   )

}

Complexidade quase igual à chamada de uma simples função.

Conclusão

Hooks vieram para nos dar uma forma nova de lidar com componentes complexos e compartilhamento de lógica entre eles, é um boa pedida para quem curte uma abordagem mais simplista e funcional de arquitetura de componentes.

Vale ressaltar que Hooks não vêm para substituir a forma tradicional com classes. Esta continuará funcionando, apenas aumentamos nosso leque de opções na construção de componentes.

Referencias

Documentação oficial

Tweet about this on TwitterShare on FacebookShare on LinkedInEmail this to someone
Comments

Leave a Comment