Qual é a diferença entre useCallback e useMemo na prática?

89

Talvez eu tenha entendido mal alguma coisa, mas useCallback Hook é executado sempre que ocorre uma nova renderização.

Passei entradas - como um segundo argumento para usarCallback - constantes não sempre mutáveis ​​- mas o retorno de chamada memoized ainda executa meus cálculos caros em cada renderização (tenho certeza - você pode verificar por si mesmo no trecho abaixo).

Eu mudei useCallback para useMemo - e useMemo funciona conforme o esperado - é executado quando as entradas são alteradas. E realmente memoriza os cálculos caros.

Exemplo ao vivo:

'use strict';

const { useState, useCallback, useMemo } = React;

const neverChange = 'I never change';
const oneSecond = 1000;

function App() {
  const [second, setSecond] = useState(0);
  
  // This 👇 expensive function executes everytime when render happens:
  const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
  const computedCallback = calcCallback();
  
  // This 👇 executes once
  const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
  
  setTimeout(() => setSecond(second + 1), oneSecond);
  
  return `
    useCallback: ${computedCallback} times |
    useMemo: ${computedMemo} |
    App lifetime: ${second}sec.
  `;
}

const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };

function expensiveCalc(hook) {
  let i = 0;
  while (i < tenThousand) i++;
  
  return ++expensiveCalcExecutedTimes[hook];
}


ReactDOM.render(
  React.createElement(App),
  document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

java-man-script
fonte
1
Eu não acho que você precisa ligar computedCallback = calcCallback();. computedCallbackdeve ser apenas = calcCallback , it will update the callback once neverChange` alterações.
Noitidart
1
useCallback (fn, deps) é equivalente a useMemo (() => fn, deps).
Henry Liu

Respostas:

155

TL; DR;

  • useMemo é memorizar um resultado de cálculo entre as chamadas de uma função e entre renderizações
  • useCallback é memorizar o próprio retorno de chamada (igualdade referencial) entre renderizações
  • useRef é manter os dados entre as renderizações (a atualização não dispara uma nova renderização)
  • useState é manter os dados entre as renderizações (a atualização irá disparar uma nova renderização)

Versão longa:

useMemo concentra-se em evitar cálculos pesados.

useCallbackconcentra-se em uma coisa diferente: corrige problemas de desempenho quando manipuladores de eventos em linha como onClick={() => { doSomething(...); }causaPureComponent a re-renderização do filho (porque as expressões de função são referencialmente diferentes a cada vez)

Dito isso, useCallbackestá mais perto deuseRef , em vez de uma forma de memorizar um resultado de cálculo.

Olhando para os documentos, eu concordo que parece confuso lá.

useCallbackretornará uma versão memoized do callback que só muda se uma das entradas mudou. Isso é útil ao passar callbacks para componentes filhos otimizados que dependem da igualdade de referência para evitar renderizações desnecessárias (por exemplo, shouldComponentUpdate).

Exemplo

Suponha que tenhamos um PureComponentfilho com base em <Pure />que seria renderizado novamente apenas depois de propsalterado.

Este código renderiza novamente o filho cada vez que o pai é renderizado novamente - porque a função inline é referencialmente diferente a cada vez:

function Parent({ ... }) {
  const [a, setA] = useState(0);
  ... 
  return (
    ...
    <Pure onChange={() => { doSomething(a); }} />
  );
}

Podemos lidar com isso com a ajuda de useCallback:

function Parent({ ... }) {
  const [a, setA] = useState(0);
  const onPureChange = useCallback(() => {doSomething(a);}, []);
  ... 
  return (
    ...
    <Pure onChange={onPureChange} />
  );
}

Mas uma vez que aé alterada, descobrimos que a onPureChangefunção de manipulador que criamos - e React lembrou para nós - ainda aponta para o avalor antigo ! Temos um bug em vez de um problema de desempenho! Isso ocorre porque onPureChangeusa um fechamento para acessar a avariável, que foi capturada quando onPureChangefoi declarada. Para corrigir isso, precisamos informar ao React onde soltar onPureChangee recriar / lembrar (memorizar) uma nova versão que aponta para os dados corretos. Fazemos isso adicionando acomo uma dependência no segundo argumento para `useCallback:

const [a, setA] = useState(0);
const onPureChange = useCallback(() => {doSomething(a);}, [a]);

Agora, se afor alterado, o React renderiza novamente o componente. E durante a re-renderização, ele vê que a dependência paraonPureChange é diferente e há uma necessidade de recriar / memorizar uma nova versão do retorno de chamada. Finalmente tudo funciona!

NB não apenas para PureComponent/ React.memo, igualdade referencial pode ser crítica quando se usa algo como uma dependência em useEffect.

Skyboyer
fonte
19

One-liner para useCallbackvs useMemo:

useCallback(fn, deps)é equivalente a useMemo(() => fn, deps).


Com useCallbacksuas funções memoize, useMemomemoriza qualquer valor calculado:

const fn = () => 42 // assuming expensive calculation here
const memoFn = useCallback(fn, [dep]) // (1)
const memoFnReturn = useMemo(fn, [dep]) // (2)

(1)retornará uma versão memoized de fn- mesma referência em vários renderizadores, desde que depseja o mesmo. Mas cada vez que você invoca memoFn , esse cálculo complexo começa novamente.

(2)irá invocar fntoda vez que depmudar e lembrar de seu valor retornado ( 42aqui), que é então armazenado em memoFnReturn.

ford04
fonte
18

Você está chamando o retorno de chamada memoized todas as vezes, ao fazer:

const calcCallback = useCallback(() => expensiveCalc('useCallback'), [neverChange]);
const computedCallback = calcCallback();

É por isso que a contagem de useCallbackestá aumentando. No entanto, a função nunca muda, ela nunca cria **** um novo retorno de chamada, é sempre o mesmo. SignificadouseCallback está fazendo seu trabalho corretamente.

Vamos fazer algumas alterações em seu código para ver se isso é verdade. Vamos criar uma variável global,, lastComputedCallbackque controlará se uma nova função (diferente) for retornada. Se uma nova função for retornada, isso significa useCallbackapenas "executado novamente". Então quando for executado novamente a gente chama expensiveCalc('useCallback'), pois é assim que você está contando se useCallbackfuncionou. Eu faço isso no código abaixo e agora está claro que useCallbackestá memoizing conforme o esperado.

Se você quiser useCallbackrecriar a função todas as vezes, descomente a linha no array que passa second. Você o verá recriar a função.

'use strict';

const { useState, useCallback, useMemo } = React;

const neverChange = 'I never change';
const oneSecond = 1000;

let lastComputedCallback;
function App() {
  const [second, setSecond] = useState(0);
  
  // This 👇 is not expensive, and it will execute every render, this is fine, creating a function every render is about as cheap as setting a variable to true every render.
  const computedCallback = useCallback(() => expensiveCalc('useCallback'), [
    neverChange,
    // second // uncomment this to make it return a new callback every second
  ]);
  
  
  if (computedCallback !== lastComputedCallback) {
    lastComputedCallback = computedCallback
    // This 👇 executes everytime computedCallback is changed. Running this callback is expensive, that is true.
    computedCallback();
  }
  // This 👇 executes once
  const computedMemo = useMemo(() => expensiveCalc('useMemo'), [neverChange]);
  
  setTimeout(() => setSecond(second + 1), oneSecond);
  return `
    useCallback: ${expensiveCalcExecutedTimes.useCallback} times |
    useMemo: ${computedMemo} |
    App lifetime: ${second}sec.
  `;
}

const tenThousand = 10 * 1000;
let expensiveCalcExecutedTimes = { 'useCallback': 0, 'useMemo': 0 };

function expensiveCalc(hook) {
  let i = 0;
  while (i < 10000) i++;
  
  return ++expensiveCalcExecutedTimes[hook];
}


ReactDOM.render(
  React.createElement(App),
  document.querySelector('#app')
);
<h1>useCallback vs useMemo:</h1>
<div id="app">Loading...</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

Benefício de useCallbacké que a função retornado é o mesmo, então reagir não é removeEventListener'ing e addEventListenering no elemento cada vez, a menos que as computedCallbackalterações. E as computedCallbackúnicas mudanças quando as variáveis ​​mudam. Assim, reagirá apenas addEventListeneruma vez.

Ótima pergunta, aprendi muito respondendo-a.

Noitidart
fonte
2
apenas um pequeno comentário para uma boa resposta: o objetivo principal não é sobre addEventListener/removeEventListener(este op em si não é pesado, pois não leva ao refluxo / repintura do DOM), mas para evitar a re-renderização PureComponent(ou com personalizado shouldComponentUpdate()) filho que usa esse retorno de chamada
skyboyer
Obrigado @skyboyer eu não tinha ideia de *EventListenerser barato, isso é um grande ponto sobre isso não causar refluxo / pintura! Sempre pensei que era caro, então tentei evitar. Portanto, no caso de eu não estar passando para a PureComponent, a complexidade adicionada useCallbackvale a pena compensar por ter o react e o DOM fazendo complexidade extra remove/addEventListener?
Noitidart
1
se não usar PureComponentou customizar shouldComponentUpdatepara componentes aninhados, então useCallbacknão adicionará nenhum valor (a sobrecarga pela verificação extra do segundo useCallbackargumento anulará o salto de removeEventListener/addEventListenermovimento extra )
skyboyer
Uau, super interessante, obrigado por compartilhar isso, é um visual totalmente novo sobre como *EventListener não é uma operação cara para mim.
Noitidart
2

useMemoe useCallbackusar memoização.

Gosto de pensar em memoização como a lembrança de algo .

Enquanto ambos useMemoe useCallback lembram de algo entre renderizações até que as dependências mudem, a diferença é exatamente o que eles lembram .

useMemoirá lembrar o valor retornado de sua função.

useCallbackvai se lembrar de sua função real.

Fonte: Qual é a diferença entre useMemo e useCallback?

Atomitos
fonte