Não é segredo que o React.js tem se tornado amplamente popular nos últimos anos. É agora a biblioteca JavaScript de escolha para muitos dos jogadores mais proeminentes da internet, incluindo o Facebook e o WhatsApp.

Uma das principais razões para sua ascensão foi a introdução de hooks na versão 16.8. Os hooks do React permitem que você utilize a funcionalidade React sem escrever componentes de classe. Agora os componentes funcionais com hooks se tornaram a estrutura de go-to-work dos desenvolvedores para trabalhar com o React.

Neste artigo do blog, vamos nos aprofundar em um hook específico – useCallback – porque ele toca em uma parte fundamental da programação funcional conhecida como memoization. Você saberá exatamente como e quando utilizar o hook useCallback e fazer o melhor de suas capacidades de melhoria de desempenho.

Pronto? Vamos mergulhar!

O que é Memoization?

Memoization é quando uma função complexa armazena sua saída para que na próxima vez seja chamada com a mesma entrada. É semelhante ao cache, mas em um nível mais local. Ela pode pular qualquer cálculo complexo e retornar a saída mais rapidamente, como já está calculado.

Isso pode ter um efeito significativo na alocação e desempenho da memória, e essa tensão é o que hook useCallback pretende aliviar.

useCallback vs usarMemo

Neste ponto, vale a pena mencionar que useCallback se emparelha bem com outro hook chamado useMemo. Vamos discutir os dois, mas nesta peça, vamos focar em useCallback como o tópico principal.

A diferença chave é que useMemo retorna um valor memorizado, enquanto useCallback retorna uma função memorizada. Isso significa que useMemo é usado para armazenar um valor computado, enquanto useCallback retorna uma função que você pode chamar mais tarde.

Estes hooks lhe devolverão uma versão em cache a menos que uma de suas dependências (por exemplo, estado ou adereços) mude.

Vamos dar uma olhada nas duas funções em ação:

import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]

// This will always return the same value, a sorted array. Once the values array changes then this will recompute.
const memoizedValue = useMemo(() => values.sort(), [values])

// This will give me back a function that can be called later on. It will always return the same result unless the values array is modified.
const memoizedFunction = useCallback(() => values.sort(), [values])

O trecho de código acima é um exemplo elaborado, mas mostra a diferença entre os dois callbacks:

  1. memoizedValue se tornará a matriz [1, 2, 3, 4, 6, 9]. Enquanto a variável de valores permanecer, o mesmo acontecerá com memoizedValue, e ela nunca mais será recalculada.
  2. memoizedFunction será uma função que retornará o array [1, 2, 3, 4, 6, 9].

O que é ótimo nessas duas ligações é que elas ficam em cache e ficam penduradas até que a matriz de dependência mude. Isto significa que em um renderizador, eles não receberão lixo coletado.

Rendering e React

Por que a memorização é importante quando se trata do React?

Tem a ver com a forma como o React torna seus componentes. React usa um DOM Virtual armazenado na memória para comparar dados e decidir o que atualizar.

O DOM virtual ajuda o React com desempenho e mantém sua aplicação rápida. Por padrão, se algum valor em seu componente mudar, o componente inteiro será renderizado novamente. Isso torna o React “reativo” à entrada do usuário e permite que a tela seja atualizada sem recarregar a página.

Você não quer renderizar o seu componente porque as mudanças não afetarão esse componente. Aqui é onde a memorização através de useCallback e useMemo vem a calhar.

Quando o React religa o seu componente, ele também recria as funções que você declarou dentro do seu componente.

Note que ao comparar a igualdade de uma função com outra função, elas sempre serão falsas. Como uma função também é um objeto, ela só será igual a si mesma:

// these variables contain the exact same function but they are not equal
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')

hello === hello2 // false
hello === hello // true

Em outras palavras, quando o React re-entrega seu componente, ele verá quaisquer funções que são declaradas em seu componente como sendo novas funções.

Isto é bom na maioria das vezes, e funções simples são fáceis de computar e não terão impacto no desempenho. Mas as outras vezes quando você não quer que a função seja vista como uma nova função, você pode confiar em useCallback para ajudá-lo.

Você pode estar pensando: “Quando eu não gostaria que uma função fosse vista como uma nova função? Bem, há certos casos em que useCallback faz mais sentido:

  1. Você está passando a função para outro componente que também é memorizado (useMemo)
  2. Sua função tem um estado interno que precisa ser lembrado
  3. Sua função é uma dependência de outro hook, como useEffect por exemplo

Benefícios de performance do useCallback

Quando useCallback é usado apropriadamente, ele pode ajudar a acelerar o seu aplicativo e evitar que os componentes voltem a renderizar se eles não precisarem.

Suponha, por exemplo, que você tem um componente que recupera uma grande quantidade de dados e é responsável por exibir esses dados na forma de um gráfico ou diagrama, como este aqui:

Gráfico de barras gerado usando um componente React
Gráfico de barras gerado usando um componente React

Suponha que o componente principal para a sua visualização de dados seja o componente que você reapresenta, mas os adereços ou estado alterados não afetam esse componente. Nesse caso, você provavelmente não quer ou precisa renderizá-lo e recuperar todos os dados. Evitar esta renderização e a restauração pode economizar a largura de banda do seu usuário e proporcionar uma experiência mais suave para o usuário.

Desvantagens do useCallback

Embora este hook possa ajudá-lo a melhorar seu desempenho, ele também vem com suas armadilhas. Algumas coisas a considerar antes de usar useCallback (e useMemo) são:

  • Garbage collection: As outras funções que ainda não estão memorizadas serão jogadas fora pelo React para liberar a memória.
  • Alocação de memória: Similar à coleta de lixo, quanto mais funções memotizadas você tiver, mais memória será necessária. Além disso, cada vez que você usar essas chamadas, há um monte de código dentro do React que precisa usar ainda mais memória para lhe fornecer a saída em cache.
  • Complexidade do código: Quando você começa a envolver funções nestes hooks, você imediatamente aumenta a complexidade do seu código. Agora é necessário entender melhor porque estes hooks estão sendo usados e confirmar que eles são usados corretamente.

Estar ciente das armadilhas acima pode poupar a você mesmo a dor de cabeça de tropeçar nelas. Ao considerar empregar o useCallback, tenha certeza de que os benefícios de desempenho superarão os inconvenientes.

Exemplo de React useCallback

Abaixo está uma configuração simples com um componente de Button e um componente de Counter. O Counter tem duas partes de estado e apresenta dois componentes de Button , cada um que atualizará uma parte separada do estado dos componentes do Counter.

O componente Button recebe dois adereços: handleClick e name. Cada vez que o Botão for renderizado, ele irá se conectar ao console.

import { useCallback, useState } from 'react'

const Button = ({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
}

const Counter = () => {

console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  return (
    <>
      {countOne} {countTwo}
      <Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
      <Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
    </>
  )
}

Neste exemplo, sempre que você clicar em um dos botões, você verá isso no console:

// counter rendered

// button1 rendered
// button2 rendered

Agora, se aplicarmos useCallback às nossas funções handleClick e embrulharmos nosso Botão em React.memo, podemos ver o que useCallback nos fornece. React.memo é similar a useMemo e nos permite memorizar um componente.

import { useCallback, useState } from 'react'

const Button = React.memo(({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
})

const Counter = () => {
  console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
  const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
  return (
    <>
        {countOne} {countTwo}
        <Button handleClick={memoizedSetCountOne} name="button1" />
        <Button handleClick={memoizedSetCountTwo} name="button1" />
    </>
  )
}

Agora, quando clicarmos em qualquer um dos botões, veremos apenas o botão que clicamos para entrar no console:

// counter rendered

// button1 rendered

// counter rendered

// button2 rendered

Nós aplicamos a memorização ao nosso componente Button, e os valores de adereço que são passados a ele são vistos como iguais. As duas funções handleClick estão em cache e serão vistas como a mesma função do React até que o valor de um item na matriz de dependência mude (por exemplo countOne, countTwo).

Resumo

Por mais legais que sejam useCallback e useMemo, lembre-se que eles têm casos de uso específico – você não deve estar envolvendo todas as funções com estes hooks. Se a função for computacionalmente complexa, uma dependência de outro hook ou um adereço passado para um componente memorizado são bons indicadores que você pode querer alcançar para useCallback.

Esperamos que este artigo o tenha ajudado a entender esta avançada funcionalidade React e ajudado a ganhar mais confiança com a programação funcional ao longo do caminho!

Matthew Sobieray

Matthew works for Kinsta as a Development Team Lead from his home office in Denver, Colorado. He loves to learn, especially when it comes to web development.