Front End

29 mar, 2019

Simplificando componentes com React Hooks

Publicidade

Introduzida 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 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 várias 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 precisarão ser limpos no ComponentWillUnmount – podemos considerar que buscar dados na API e definir event listeners não têm 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, além 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, além 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, tornando menos viável a reutilização de um componente.

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 ajuda 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, além de ocasionar problemas de performance ou efeitos colaterais não esperados.

Como solução temos o React Hooks, que permite que você extraia 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 diretamente 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.

Da seguinte maneira:

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

Dessa forma 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.

Além 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 o 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 página será alterado de acordo com a atualização do estado do componente. Na prática, o useEffect nesse contexto é equivalente ao ComponentDidMount e também ao ComponentDidUpdate. Ele invocará a função passada quando o componente é montado e quando é atualizado.

O useEffect também te dá 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>
)}

Neste 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 conta 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 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 vamos 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á vêm por padrão na biblioteca do React, mas vale abordar brevemente dois deles. O useContext, que dá acesso direto à API de contexto do react e permite que passemos dados de um componente 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>
    </>
  );
}

Neste exemplo o dispatch 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.

Essa 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 precisaremos 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, 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 da seguinte 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.

Referências

***

Este artigo foi produzido em parceria com a Lambda3. Leia outros conteúdos no blog da empresa: blog.lambda3.com.br