React.memo & useMemo & useCallbackPrimeiramente, a gente tem que entender como que funciona o processo de re-renderização do React, e aí sim entender quando e como que a memorização pode ser útil.
Basicamente, podemos dizer que toda re-renderização do React.js é causada por conta de um estado que foi alterado.
Mas isso não significa que apenas o estado do componente é responsável por uma re-renderização. Calma! Vamos entender isso com calma.
A mudança de estado é o inicio de tudo, mas muita coisa acontece por conta disso e a gente tem que entender como que o React.js funciona por baixo dos panos.
Vamos focar em dois principais gatilhos que fazem um componente ser re-renderizado;
Se um estado que ta sendo passado por propriedade pra um componente é alterado, logo, esse componente precisa ser re-renderizado também, por conta disso sempre que uma propriedade de um componente muda, ele é re-renderizado.
Quando um componente é re-renderizado, tudo nele é “recriado“ novamente, e isso inclui tudo que está no return também, então se um componente pai é re-renderizado, todos os seus filhos também são re-renderizados.
É um pouco confuso no começo, mas na prática é um pouco mais fácil de entender o que está acontecendo por trás dos panos.
Então vamos iniciar pensando em um componente assim:
export const App = () => {const [switcherValue, setSwitcherValue] = useState(false);const handleToggle = () => setSwitcherValue((prev) => !prev);return (<div><Title>Without Memo</Title><SwitcherContainer onToggle={handleToggle} enabled={switcherValue} /></div>);}
Nesse exemplo a gente consegue perceber o básico do react funcionando.
O componente App está sendo renderizado toda vez que o switcherValue é alterado.
O componente SwitcherContainer está sendo renderizado toda vez que o estado switcherValue é alterado, por que o switcherValue está sendo passado como propriedade pra ele, logo ele realmente precisa ser re-renderizado!
E a gente consegue perceber também, que o Title também foi re-renderizado, apenas por ser um componente filho de um componente que está sendo re-renderizado.
Nesse momento já conseguimos pensar no nosso primeiro caso de memorização.
Afinal, não tem a menor necessidade do componente Title renderizar junto, sendo que ele é basicamente um “componente estático“, nenhuma propriedade dele foi alterada.
É aí que entra a primeira mágica, o React.memo, que basicamente faz com que um componente só seja re-renderizado se alguma propriedade que ele recebe for alterada. Mesmo que o componente pai seja re-renderizado.
Logo, isso resolveria nosso problema:
export const Title = React.memo(({ children }: TitleProps) => {return (<h1>{children}</h1>);});Title.displayName = "Title";
E o componente App continuaria igual
export const App = () => {const [switcherValue, setSwitcherValue] = useState(false);const handleToggle = () => setSwitcherValue((prev) => !prev);return (<div><Title>With Memo</Title><SwitcherContainer onToggle={handleToggle} enabled={switcherValue} /></div>);}
Agora, o Title não é re-renderizado mais, por que nenhuma propriedade nem estado dele estão sendo alterados.
O SwitcherContainer continua re-renderizando por que ele realmente depende do estado, não é um re-render desnecessário.
Até então ta muito fácil, na vida real nem sempre é assim tão simples, vamos complicar um pouco mais agora. E se nesse componente, a gente conseguisse alterar o estado a partir de dois lugares diferentes...
export const App = () => {const [switcherValue, setSwitcherValue] = useState(false);const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);const handleToggleSwitcherValue = () => setSwitcherValue((prev) => !prev);const handleToggleAnotherSwitcherValue = () => setAnotherSwitcherValue((prev) => !prev);return (<div><Title>Without Memo</Title><SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} /><SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} /></div>);}
Bom... O mesmo problema que tivemos com o Title certo? Um SwitcherContainer está sendo re-renderizado quando eu altero o valor do outro. Só adicionar um React.memo que vai ser resolvido.
E se eu te falasse que o SwitcherContainer já está usando o React.memo?
Então o que está acontecendo? Nesse caso, o problema é um pouco diferente...
Note que uma das propriedades que está sendo passada pro SwitcherContainer é uma função (onToggle).
Quando comparamos duas funções javascript, elas são consideradas iguais apenas se elas tiverem a mesma referência.
Ou seja, mesmo se o conteúdo das funções forem exatamente IGUAL, se ela tiver sido “recriada“, o javascript vai considerar que são funções diferentes.
Exemplo:
const function1 = () => console.log("Hello World");const function2 = () => console.log("Hello World");console.log(function1 === function2); // falseconst function3 = function1;console.log(function1 === function3); // true
Então o que está acontecendo é:
App é re-renderizado;onToggle são recriadas;SwitcherContainer foram alteradas;SwitcherContainer;Como resolver isso? Aí que entra o segundo feitiço, o useCallback
O useCallback basicamente memoriza a função e faz com que ela não seja re-criada toda vez que o componente é re-renderizado, preservando a referência da função e assim evitando re-render desnecessário por conta do React achar que alguma propriedade mudou.
É como se em cada re-render, o React invés de “recriar“ a função, ele reutilizasse a mesma que ele guardou na memória (caso nenhuma dependencia tenha sido alterada)
export const App = () => {const [switcherValue, setSwitcherValue] = useState(false);const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);const handleToggleSwitcherValue = useCallback(() => setSwitcherValue((prev) => !prev), []);const handleToggleAnotherSwitcherValue = useCallback(() => setAnotherSwitcherValue((prev) => !prev), []);return (<div><Title>Without Memo</Title><SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} /><SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} /></div>);};
Agora sim! Agora o React consegue identificar exatamente quando a função é alterada de fato e a gente consegue evitar o re-render desnecessário!
OBS: É obrigatório o uso do React.memo, caso contrário, nessa situação, o useCallback não terá efeito nenhum, sem o React.memo cairá na mesma situação do primeiro cenário, o SwitcherContainer vai re-renderizar apenas por que o componente pai re-renderizou...
Ok, agora que entedemos como o useCallback funciona, vamos complicar um pouco mais o nosso componente...
Na vida real, trabalhamos com dados vindo de diversos lugares e é muito comum utilizar estruturas como Array e Objetos pra transportar esses dados
E se o nosso componente passasse um objeto ou um array como propriedade pra algum filho?
export const App = () => {const [switcherValue, setSwitcherValue] = useState(false);const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);const handleToggleSwitcherValue = useCallback(() => setSwitcherValue((prev) => !prev), []);const handleToggleAnotherSwitcherValue = useCallback(() => setAnotherSwitcherValue((prev) => !prev), []);const user = {name: "John",age: 25,}return (<div><Title>Without Memo</Title><User user={user} /><SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} /><SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} /></div>);};
Name: John
Age: 25
Bom, o mesmo problema que tivemos com as funções, acontece com os objetos/arrays também...
Com o javascript é a mesma coisa, se você criar um objeto novo, ele vai ter uma referência diferente do objeto anterior, mesmo que o conteúdo seja o mesmo.
Exemplo:
const object1 = { name: "John", age: 25 };const object2 = { name: "John", age: 25 };console.log(object1 === object2); // falseconst object3 = object1;console.log(object1 === object3); // true
Então o que está acontecendo é:
App é re-renderizado;user é recriado;User foram alteradas;User;Mas dessa vez não podemos usar o useCallback pra resolver isso, por sorte temos um cara tão bom quanto chamado useMemo que faz exatamente a mesma coisa, mas para dados estáticos como objetos e arrays. (Ou qualquer coisa que não seja uma função)
export const App = () => {const [switcherValue, setSwitcherValue] = useState(false);const [anotherSwitcherValue, setAnotherSwitcherValue] = useState(false);const handleToggleSwitcherValue = useCallback(() => setSwitcherValue((prev) => !prev), []);const handleToggleAnotherSwitcherValue = useCallback(() => setAnotherSwitcherValue((prev) => !prev), []);const user = useMemo(() => ({name: "John",age: 25,}), [])return (<div><Title>With Memo</Title><User user={user} /><SwitcherContainer onToggle={handleToggleSwitcherValue} enabled={switcherValue} /><SwitcherContainer onToggle={handleToggleAnotherSwitcherValue} enabled={anotherSwitcherValue} /></div>);};
Name: John
Age: 25
Agora sim! O User não é re-renderizado mais
Agora o que está acontecendo é:
App é re-renderizado;user não é recriado, por conta do useMemo;User não foram alteradas;User não é re-renderizado novamente;OBS: Mesma coisa do useCallback. É obrigatório o uso do React.memo, caso contrário, nessa situação, não terá efeito nenhum, sem o React.memo cairá na mesma situação do primeiro cenário lá em cima, o User vai acabar sendo re-renderizado apenas por que o componente pai re-renderizou...
useMemo e o useCallbackTerão vezes que a gente vai precisar usar o useMemo e o useCallback pra resolver outras coisas que não sejam apenas evitar re-renderizações desnecessárias.
Os exemplos que eu dei eram todos focados em evitar re-renderizações em conjunto com o React.memo, mas eles tem outras utilidades também.
Por exemplo, o useMemo pode ser usado pra fazer cálculos pesados e evitar que eles sejam refeitos toda vez que o componente é re-renderizado.
const sum = useMemo(() => {let result = 0;for (let i = 0; i < 1000000000; i++) {result += i;}return result;}, [])
Imagina toda hora que um componente re-renderizar (agora que você sabe como funciona) ele ter que re-fazer esse cálculo de novo... Não é muito legal né?
O useCallback e useMemo também pode ser usado pra evitar disparar um useEffect desnecessariamente, por exemplo.
const handleFunction = useCallback(() => {console.log("Hello World");}, []);useEffect(() => {handleFunction();}, [handleFunction]);
Sem o useCallback nesse caso, toda vez que o componente re-renderizar, ohandleFunction seria recriado, e o useEffect seria disparado, por que ele não conseguiria identificar que a função é a mesma. Por conta que a referência da função mudou.
E o mesmo vale pro useMemo, se você tiver colocando objetos ou arrays como dependências de um useEffect, por exemplo, e toda vez que o componente re-renderizar, ele vai recriar o objeto e o useEffect vai ser disparado, por que ele não vai conseguir identificar que o objeto é o mesmo.
Mas isso foi apenas pra mostrar que o useCallback e o useMemo não server apenas pra serem utilizado em conjunto com o React.memo, eles tem outras utilidades também.
Claro que os exemplos que eu dei aqui são bem simples e não refletem necessariamente a realidade de todas as aplicações, mas a ideia é mostrar como o React.memo, useMemo e o useCallback podem ser úteis em situações do dia a dia.
Muitas vezes acabam usando o useMemo e o useCallback sem realmente entender o que está acontecendo, e a longo prazo isso pode acabar se tornando um problema. Por mais que o custo seja pequeno, o useMemo e o useCallback tem um custo de performance, afinal, eles estão fazendo um trabalho extra por trás dos panos.
Então é sempre bom entender como eles funcionam de verdade pra saber quando realmente é necessário usá-los.
Mas também não adianta fazer um terror em cima disso...
Na minha humilde opinião, na maioria das vezes, em aplicações pequenas, o uso deuseMemo e useCallback em excesso não vai causar um impacto tão grande na performance... Teria que ser um uso muito grande pra isso começar realmente a ficar perceptível.
É mais fácil um problema de performance ser causado por conta da ausencia deles do que pelo excesso deles. Mas use com moderação! 😂
Fique a vontade pra instalar o repositório e fazer mais testes por conta própria!
React.memo, etc... O novo compilador até então está em beta, mas já da pra ser testado por quem quiser.