Introdução

Fala pessoal, esse é mais outro post/vídeo, e como o título diz, a ideia será converter um componente para usar o novíssimo hooks. Se você é um leitor assíduo do post, já deve ter visto meu post sobre hooks, lá eu falo o que são, para que servem e outras coisas mais.

O componente escolhido para converter foi o React-Snakke feito pelo Diogo Moretti.

Você pode ver o código todo nesse link do CodeSandbox e abaixo deixarei o vídeo também.

Vídeo

Sobre o código e o refactor

Bom, eu vou pontuar as partes mais importantes do código aqui, mas eu aconselho a assistir o vídeo também, já que nele eu explico mais detalhadamente o processo. Certas partes do código serão simplificadas com ..., mas você pode ver tudo lá no CodeSandbox.

Transformar de Classe para Função

Bom, a primeira etapa, é transformar o componente que usava classe, para função. Já que os hooks não funcionam dentro de classes.

// antes
export default class Snakke extends Component { ... }

// depois
export default function Snakke(props) { ... }

Remover o this e usar as variáveis diretamente

Feito isso, a outra coisa é que todos os lugares que tiverem referências ao this, ou seja, o objeto da classe, não existem mais e você precisa usar ou direto do props ou a variável direta se for o caso. E variáveis dentro das funções precisam ser assinaladas como tal, usando let ou const.

// antes
styles = {
  ...
  height: this.props.height,
  opacity: this.props.opacity,
  zIndex: this.props.zIndex,
  ...
}

// depois
const styles = {
  ...
  height: props.height,
  opacity: props.opacity,
  zIndex: props.zIndex,
  ...
}

Substituir state pelo useState

Depois dessas duas mudanças, nós começamos a procurar locais onde tenham definições e mudanças de state.

E no caso do código inicial, nós tínhamos a variável progress que possuía um valor inicial e também recebia uma atualização através do this.setState

state = {
  progress: 0,
}

this.setState({
  progress: total,
})

Dentro do React Hooks, para trabalhar com estados, nós temos o método useState. E ele funciona da seguinte forma:

import React, { useState } from 'react'

const [progress, setProgress] = useState(0)

Nós criamos um array que recebe 2 valores, o primeiro é a variável que queremos mexer com os valores dela (nosso estado) e o segundo parâmetro é o método que vai ser responsável por atualizar esse estado, no caso o setProgress.

O último detalhe sobre o useState é que ele recebe um parâmetro, que é o estado inicial da variável, no nosso caso, é o valor 0.

Substituir métodos lifecycle por useEffect

Dentro desse componente inicial, nós usávamos o componentDidMount e componentWillUnmount, que serviam para adicionar/remover o listener de scroll no momento que o componente era montado/desmontado.

Nos hooks, nós podemos substituir essa parte pelo useEffect, ficando dessa forma:

// antes
componentDidMount() {
  window.addEventListener('scroll', this.setProgress)
}

componentWillUnmount() {
  window.removeEventListener('scroll', this.setProgress)
}

// depois
useEffect(() => {
  window.addEventListener('scroll', handleScroll)
  return () => {
    window.removeEventListener('scroll', handleScroll)
  }
}, [])

Como funciona isso? No momento que o componente renderiza na tela, o useEffect é chamado e com ele, o listener é adicionado. O mesmo método possui um cleanup que é chamado sempre quando o componente desmonta e para indicar isso, basta que você retorne uma função, que é exatamente o que fazemos, dentro do nosso return nós passamos o removeListener, assim garantimos que o listener vai ser desmontado junto do componente, sem causar problemas de memory leak/performance.

Um último detalhe do useEffect é que ele recebe como segundo parâmetro, um array, onde podemos indicar quais propriedades queremos ficar vigiando, para que o useEffect execute toda vez que essas propriedades sejam atualizadas. Se não passarmos nenhum array, o useEffect vai ser chamado em cada rerender, para evitar isso, já que só queremos adicionar o listener uma vez, nós passamos um array vazio [], isso garante que o useEffect será chamado só quando for montado e o cleanup só quando desmontado.

Substituir setState para o método criado no useState

Vimos acima que nós criamos o método setProgress certo? Ele vai servir para atualizar o progress quando desejarmos, assim como o setState fazia. Então, para que tenha essa mudança de estado, falta chamar esse método no seu devido lugar, passando o novo valor.

// antes
...
this.setState({
  progress: total,
})

// depois
...
setProgress((window.scrollY / bodyHeight) * 100)

Após ter feito essas mudanças, é possível que já esteja tudo funcionando certinho. Caso não esteja, verifique se todos os this foram removidos e se onde estava usando state já está atualizado para a variável escolhida.

Viu só? Não é tão difícil fazer essa conversão não é mesmo? =D

Antes/depois

Vamos fazer uma comparação (antes e depois) dos componentes. Abaixo segue o componente utilizando classe (antes das nossas alterações):

import React, { Component } from 'react'

export default class Snakke extends Component {
  constructor(props) {
    super(props)
    this.setProgress = this.setProgress.bind(this)
    this.getPercentageScroll = this.getPercentageScroll.bind(this)
  }

  state = {
    progress: 0
  }

  styles = {
    position: 'fixed',
    top: '0',
    left: '0',
    width: '100%',
    height: this.props.height,
    opacity: this.props.opacity,
    zIndex: this.props.zIndex,
    filter: this.props.shadow ? `drop-shadow(0 0 .1em ${this.props.color})` : 'none',
    background: `linear-gradient(to right, ${this.props.color} var(--scroll), transparent 0)`
  }

  getPercentageScroll (scrollPos) {
    const bodyHeight = document.body.clientHeight - document.documentElement.clientHeight
    return (scrollPos / bodyHeight) * 100
  }

  setProgress () {
    let total = this.getPercentageScroll(window.scrollY)

    this.setState({
      progress: total
    })
  }

  componentDidMount () {
    window.addEventListener('scroll', this.setProgress)
  }

  componentWillUnmount () {
    window.removeEventListener('scroll', this.setProgress)
  }

  render () {
    return (
      <div className='snakke-progress' style={'--scroll': `${this.state.progress}%`, ...this.styles }></div>
    )
  }
}

Snakke.defaultProps = {
  color: '#000',
  height: '5px',
  opacity: '1',
  zIndex: '9999',
  shadow: false
}

Veja abaixo o componente, já com as nossas alterações, utilizando hooks:

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

export default function Snakke(props) {
  const styles = {
    position: 'fixed',
    top: '0',
    left: '0',
    width: '100%',
    height: props.height,
    opacity: props.opacity,
    zIndex: props.zIndex,
    filter: props.shadow ? `drop-shadow(0 0 .1em ${props.color})` : 'none',
    background: `linear-gradient(to right, ${props.color} var(--scroll), transparent 0)`,
  }

  const [progress, setProgress] = useState(0)

  function handleScroll() {
    const bodyHeight = document.body.clientHeight - document.documentElement.clientHeight
    setProgress((window.scrollY / bodyHeight) * 100)
  }

  useEffect(() => {
    window.addEventListener('scroll', handleScroll)
    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])

  return <div className="snakke-progress" style={'--scroll': `${progress}%`, ...styles} />
}

Snakke.defaultProps = {
  color: '#000',
  height: '5px',
  opacity: '1',
  zIndex: '9999',
  shadow: false,
}

Conclusão

A mudança não é enorme, mas dá para ver que o código foi simplificado, não precisamos mais ficar preocupados em entender de onde o this tá vindo e para onde tá indo, qual o seu contexto. Podemos também diminuir métodos e pelo menos para mim, o código ficou bem melhor.

Lembrando que não é errado usar Classes para os componentes, a própria equipe do React diz para não sairmos convertendo tudo sem necessidade. A ideia do post é mais para mostrar como é o funcionamento de um e outro, para que caso você pegue um código em hooks, não se sinta perdido.

Se você ficou com alguma dúvida, só falar nos comentários ou me mandar mensagem, que eu ajudo =)