A partir da versão 16.8 do React, uma nova forma de trabalhar com a reatividade dos componentes foi introduzida: React Hooks.

Essa é a maior alteração na forma de trabalhar do React desde sua concepção. Vamos entender neste post as motivações de mudar a forma de trabalhar e quais sua vantagens.

Aproveito para deixar claro que NÃO é obrigatório o uso de hooks nas novas versões do React, assim como o antigo modo de trabalhar, utilizando classes, não teve seu suporte removido.

Como era?

Até então, sempre trabalhamos com dois tipos de componentes: Os stateful e os stateless. A diferença conceitual entre eles é bem simples, mas a implementação se diverge bastante.

Vamos supor que tenhamos um componente funcional (stateless) e que por motivos de negócio, precisamos transformá-lo em um componente stateful. No caso de uma lista de produtos hipotética, teríamos este cenário:

import React from "react";

function ProductList(props) {
  return (
    <ul>
      {props.products.map((product) => (
        <span key={product.id}>{product.name}</span>
      ))}
    </ul>
  );
}

Mas se quisermos permitir a edição de cada um dos produtos, precisaremos transformar totalmente este componente, muito além de apenas substituir o span por um input:

class ProductList extends React.Component {
  constructor(props) {
    super(props);
    this.state = { products: [...props.products] };
  }

  onChangeHandler(event, id) {
    const updatedProducts = this.state.products.map((product) => {
      if (product.id === id) {
        product.name = event.target.value;
      }

      return product;
    });

    this.setState({
      products: updatedProducts,
    });
  }

  render() {
    return (
      <ul>
        {this.state.products.map((product) => (
          <input
            type="text"
            key={product.id}
            value={product.name}
            onChange={(event) => this.onChangeHandler(event, product.id)}
          />
        ))}
      </ul>
    );
  }
}

Notamos aqui que para termos um estado ligado ao componente, precisamos abandonar o componente escrito de forma funcional e escrevê-lo em forma de classe, trabalhar com as particularidades do React, extendendo a classe React.Component, invocando super em seu construtor e criando o método render. Bastante trabalho.

Por que mudar?

De acordo com a própria documentação do React, existem 3 pontos principais para que fizeram eles tomarem a decisão de criar a API de Hooks:

É Difícil Reutilizar Lógica com Estado entre Componentes

Não existia uma forma de se compartilhar lógica entre os estados.

O que se acabava fazendo era criar um HOC (High-order component) para ter esse comportamento “reutilizável” em múltiplos pontos da aplicação, mas ainda assim não resolvia o problema, uma vez que para se utilizar o HOC, os componentes que viriam a trabalhar com ele precisariam receber algumas modificações também, fazendo com que deixar de usá-los com HOC em alguns casos se tornasse uma tarefa um pouco complexa.

Componentes Complexos se Tornam Difíceis de Entender

Por conta da natureza dos componentes stateful do React, é comum a lógica ficar separada em diferentes em vários ciclos de vida, e conforme o componente cresce, vai-se criando uma complexidade cognitiva. Este é um problema não só do ponto de vista estrutural, mas cria uma dificuldade grande no que diz respeito da testabilidade do código, já que a lógica está intrínseca ao ciclo de vida. 

Com os hooks, podemos criar uma divisão da lógica baseada no negócio, e não nos ciclos de vida.

Classes Confundem tanto Pessoas quanto Máquinas

Aqui a documentação toca em 2 pontos: Classes confundem pessoas e Classes confundem máquinas.

Eu tenho uma opnião diferente no que diz respeito a primeira parte.

Por mais que utilizar classes em JavaScript necessite do aprendizado de como o this funciona na linguagem, a necessidade de realizar bind nos métodos da classe, a necessidade de alternar entre componentes de classe e funcionais para utilizá-los de forma stateless e stateful e outras complexidades que existe em utilizar o React com classes (conforme vimos em exemplo acima) foram criadas pela própria equipe do React.

As regrinhas que precisam ser seguidas são fruto da lib, e não da linguagem. Por isso a criação da API de Hooks.

Sobre o segundo ponto, realmente os problemas relatados por eles, como problemas em minificação (no momento do bundling) e problemas com hot reloading, podem acontecer, mas não são tão frequentes.

Como ficou?

Vamos refatorar nosso componentes class-based para utilizar a nova API de Hooks. Os Hooks se dividem em diferentes tipos, onde os mais comuns de serem utilizados são:

  • useState
  • useEffect
  • useContext
  • useCallback
  • useMemo
  • useRef

Neste primeiro post, vamos abordar um dos mais utilzados, que é o useState.

Este Hook vem para substituir exatamente a necessidade de criar um componente class-based quando queremos apenas ter um estado relacionado ao componente.

Refatorando nosso componente ProductList, chegamos neste resultado:

export default function ProductList(props) {
  const [products, setProducts] = useState(props.products);

  const onChangeHandler = (event, id) => {
    const updatedProducts = products.map((product) => {
      if (product.id === id) {
        product.name = event.target.value;
      }

      return product;
    });

    setProducts(updatedProducts);
  };

  return (
    <ul>
      <ul>
        {products.map((product) => (
          <input
            type="text"
            key={product.id}
            value={product.name}
            onChange={(event) => onChangeHandler(event, product.id)}
          />
        ))}
      </ul>
    </ul>
  );
}

Entendendo o useState

Vamos entender o que está acontecendo aqui. Podemos notar os seguintes pontos:

  • Não estamos mais utilizando uma classe
  • Temos uma estrutura diferente sendo retornada do método useState
  • Definimos o método onChangeHandler dentro do nosso componente funcional
  • Poucas mudanças no método onChangeHandler e no retorno do componente

Dos pontos acima, o que é mais interessante discutirmos é como o useState funciona.

const [products, setProducts] = useState(props.products);

Quando invocamos o hook, podemos passar um valor para a invocação. No nosso caso, estamos passando a lista de produtos que recebemos como props. Em alguns casos, poderia ser um array vazio, ou uma string com um valor default, etc.

O valor informado na hora da invocação é o valor default do estado. Ou seja, o valor com o qual é iniciado.

Podemos já relacionar com o funcionamento do state em class-based components:

  • Nos componentes criados como classe, o state é um objeto do escopo do componente, onde poderá ter várias chaves contendo valores diferentes para cada trecho do componente.
  • Quando trabalhamos com Hooks, cada chave que existiria no state de um componente baseado em classe se torna um par de getter e setter

Ou seja, a forma de trabalhar é um pouco diferente. Enquanto num componente class-based teríamos apenas um this.state com todos os dados do state nele, com hooks nós teremos múltiplas chamadas para o useState, uma para cada chave de estado. Por exemplo:

const [products, setProducts] = useState(props.products);
const [username, setUsername] = useState("bruno.fachine");
const [selectedProducts, setSelectedProducts] = useState([]);

O retorno da invocação é um array com 2 posições ocupadas:

  • Index 0: o getter do valor do state. É o valor que está gravado no state no momento.
  • Index 1: o setter do valor no state. Chamado sempre que quisermos alterar o valor.

O uso fica bem claro no método onChangeHandler, onde utilizamos tanto o getter quanto o setter:

const onChangeHandler = (event, id) => {
  const updatedProducts = products.map((product) => {
    if (product.id === id) {
      product.name = event.target.value;
    }

    return product;
  });

  setProducts(updatedProducts);
};

Esse é um post introdutório sobre react hooks. Iremos aprofundar os conceitos e detalhar os outros hooks que estão disponíveis. No próximo post falaremos sobre o hook useEffect, que substitui o ciclo de vida de um componente class-based.

Até a próxima!