Ship It!
Published in

Ship It!

Bruno Nardini

Feb 17, 2020

5 min read

React Hooks: por que devemos colocar funções no array de dependências do useEffecs?

tl;dr: Porque o JavaScript possui first-class functions, então funções sofrem mutações tanto quanto qualquer variável.

Com a introdução dos Hooks na versão 16.8 do React, surgiu uma nova forma de utilizar suas funcionalidades usando apenas funções, com conceitos e regras diferentes dos que já conhecíamos usando classes.

Um conceito novo é o array de dependências usado nos hooks useEffect, useMemo, useCallback e useImperativeHandle. O useEffect é um dos primeiros hooks que se aprende, e é um dos mais usados.

Sua API pode ser demonstrada da seguinte forma:

useEffect(didUpdateFn, [dependencyArray]);

O array de dependência (dependencyArray) é opcional, mas é importante usá-lo para condicionar a chamada do seu callback (didUpdateFn) somente quando necessário, e ele aceita tipos primitivos, objetos e… funções! 😱

Neste artigo é demonstrado na prática o motivo de usar funções como dependência e algumas alternativas de como usá-las com eficiência.

Comparações de igualdade entre funções

Antes de começar a falar sobre os Hooks, é necessário revisitar o comportamento do JavaScript ao tentar comparar funções:

O Object.is foi introduzido na versão ES2015 e retorna um booleano indicando se os dois parâmetros possuem o mesmo valor (same-value equality) ou não. O React o utiliza para fazer as comparações das dependências, mas, como você pode ver no código acima, ele se comporta de forma parecida com o operador ===.

Você pode notar que functionA e functionB não são iguais para o JavaScript, mesmo visualmente parecendo iguais. Para que uma função seja igual a outra, é preciso que seja exatamente a mesma função, como foi o caso de comparar functionB com functionB ou comparar functionB com copyFromB, pois nos dois casos são a mesma função criada na linha 2.

Foi possível copiar uma função porque o JavaScript possui first-class functions, ou seja, as funções podem ser tratadas como qualquer outra variável, você pode clonar, pode redeclarar, usar como input e output de outra função.

Funções aninhadas

Outro conceito importante é sobre o que acontece com funções aninhadas (funções dentro de funções) dentro de componentes React. O código abaixo possui um exemplo usando função e classe:

Toda vez que os componentes forem re-renderizados será recriado a função myNestedFunction, ou seja, a cada renderização o myNestedFunction será uma nova função diferente da anterior.

Isso pode confundir um pouco devido à utilização do const que não permite uma variável ter seu valor alterado. Mas como a função inteira é chamada novamente pelo React, é uma variável nova toda vez.

O problema na prática

No exemplo abaixo está o componente App que possui dois contadores criados com o useState e utiliza dois componentes Logger que usa o useEffect para exibir uma mensagem com os valores dos contadores:

Se quiser testar o código, utilize este link do CodePen.

Dentro do useEffect está um log (linha 9) que exibirá o texto toda vez que ele for alterado. Então, sem fazer nada, só de carregar a página é feito o log:

Contador A: 0
Contador B: 0

Porém, se removermos o array de dependência [index, getCounter] da linha 10, já na primeira renderização teríamos esse log:

Contador A: 0
Contador B: 0
Contador A: 0
Contador B: 0

O array de dependência garante a execução do callback do useEffect só quando os dados mudarem, senão ele vai ser executado após cada renderização.

Colocar a função no array de dependência notifica o hook que ela foi recriada e executa novamente seu callback.

O index está no array pelo fato de ter sido utilizado dentro do useEffect, mesmo ele estando fixo no exemplo, é importante deixá-lo para caso alguém faça uma alteração e ele passe a não ser mais fixo.

Com o array de dependência [index, getCounter], depois da página carregada, se clicarmos três vezes no botão “Incrementar contador A” e em seguida três vezes no botão “Incrementar contador B”, aparecerá no console:

Contador A: 1
Contador B: 0
Contador A: 2
Contador B: 0
Contador A: 3
Contador B: 0
Contador A: 3
Contador B: 1
Contador A: 3
Contador B: 2
Contador A: 3
Contador B: 3

Repare que ele imprime o log dos dois contadores mesmo quando só um deles é alterado. Isso se deve ao fato dos dois estados estarem no componente App e, a cada mudança do estado, é feito uma nova renderização e as funções getCounterA e getCounterB são recriadas juntas.

A recriação de uma função não significa que seu contexto mudou, assim como a mudança de seu contexto não significa que a função foi recriada. Recriar uma função só significa que ela foi recriada, nada mais que isso.

Para que esse exemplo funcione como o esperado, a função getCounterA deve ser recriada somente se o estadocounterA for alterado, o mesmo para o outro estado.

Otimizações

Continuando com o exemplo anterior, a função getCounter da propriedade do componente Logger retorna um número que usamos dentro do useEffect. Uma alternativa para notificar a mudança desse número seria passar o próprio número como dependência:

Se repetirmos o teste de clicar três vezes no botão “Incrementar contador A” e em seguida três vezes no botão “Incrementar contador B”, o resultado no console ficaria:

Contador A: 1
Contador A: 2
Contador A: 3
Contador B: 1
Contador B: 2
Contador B: 3

Somente o componente que recebe o contador modificado que executa o callback do useEffect.

Porém, isso vem com um preço, em toda renderização do Logger será executada a função getCounter, além dessa execução acontecer durante a renderização. Já da forma anterior era executada após a renderização dentro do callback do useEffect.

A outra alternativa seria fazer que a função getCounter seja recriada somente se o contador for alterado:

Através do hook useCallback foi possível criar uma função memoizado até que seu array de dependência identificasse uma alteração. O resultado para o mesmo teste seria o mesmo no console:

Contador A: 1
Contador A: 2
Contador A: 3
Contador B: 1
Contador B: 2
Contador B: 3

O problema dessa alternativa é que para arrumar o useEffect do Logger foi preciso alterar o App, então todos os outros componentes que vierem a utilizar o Logger teriam que fazer o mesmo.

Ambas as soluções cumprem o objetivo de construir um array de dependência para o useEffect com mais eficiência, mas não utilize essas abordagens como regras, o importante é entender os conceitos apresentados aqui para tomar uma decisão melhor em cada caso.

Considerações finais

O React oferece um plugin do ESLint com a regra exhaustive-deps que verifica o array de dependências para os Hooks e notifica se você esqueceu de alguma dependência. Na maioria dos casos existe um autofix. Esse plugin já vem por padrão nos projetos criados pelo Create React App.

Neste artigo, além de explicar o motivo da necessidade de colocar a função como dependência no exemplo usado, também foram demonstradas algumas formas de otimização para evitar renderizações extras.

Toda otimização vem com um preço, por isso meça a performance antes e depois da otimização para ver se a mudança realmente foi para melhor, e analise se os benefícios da otimização valem o seu preço.

Conteúdo, opinião, vivência e compartilhamento de ideias da equipe de Produto e Engenharia da RD Station @RD