useCallback
useCallback
é um Hook do React que permite armazenar em cache uma definição de função entre re-renderizações.
const cachedFn = useCallback(fn, dependencies)
Referência
useCallback(fn, dependencies)
Chame useCallback
na raiz do seu componente para armazenar em cache uma definição de função entre re-renderizações:
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
Parâmetros
-
fn
: O valor da função que você deseja armazenar em cache. Pode receber quaisquer argumentos e retornar quaisquer valores. O React retornará (não chamará!) sua função de volta durante a renderização inicial. Nas próximas renderizações, o React lhe dará a mesma função novamente se asdependencies
não tiverem mudado desde a última renderização. Caso contrário, ele lhe fornecerá a função que você passou durante a renderização atual e a armazenará para que possa ser reutilizada mais tarde. O React não chamará sua função. A função é retornada para que você possa decidir quando e se chamá-la. -
dependencies
: A lista de todos os valores reativos referenciados dentro do código dafn
. Os valores reativos incluem props, estado e todas as variáveis e funções declaradas diretamente dentro do corpo do seu componente. Se o seu linter estiver configurado para React, ele verificará se cada valor reativo está corretamente especificado como uma dependência. A lista de dependências deve ter um número constante de itens e ser escrita em linha como[dep1, dep2, dep3]
. O React comparará cada dependência com seu valor anterior usando o algoritmo de comparaçãoObject.is
.
Retornos
Na renderização inicial, useCallback
retorna a função fn
que você passou.
Durante renderizações subsequentes, ele retornará uma função fn
já armazenada da última renderização (se as dependências não mudaram), ou retornará a função fn
que você passou durante esta renderização.
Ressalvas
useCallback
é um Hook, portanto, você só pode chamá-lo na raiz do seu componente ou nos seus próprios Hooks. Você não pode chamá-lo dentro de loops ou condições. Se precisar disso, extraia um novo componente e mova o estado para ele.- O React não descartará a função armazenada em cache a menos que haja um motivo específico para isso. Por exemplo, em desenvolvimento, o React descarta o cache quando você edita o arquivo do seu componente. Tanto em desenvolvimento quanto em produção, o React descartará o cache se seu componente suspender durante a montagem inicial. No futuro, o React pode adicionar mais recursos que aproveitam o descarte do cache - por exemplo, se o React adicionar suporte interno para listas virtualizadas no futuro, faria sentido descartar o cache para itens que rolam para fora da área de visualização da tabela virtualizada. Isso deve corresponder às suas expectativas se você depender de
useCallback
como uma otimização de desempenho. Caso contrário, uma variável de estado ou uma ref podem ser mais apropriadas.
Uso
Ignorando re-renderizações de componentes
Quando você otimiza o desempenho de renderização, às vezes precisará armazenar em cache as funções que você passa para componentes filhos. Vamos primeiro olhar para a sintaxe de como fazer isso e, em seguida, ver em quais casos isso é útil.
Para armazenar em cache uma função entre re-renderizações do seu componente, envolva sua definição no Hook useCallback
:
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
Você precisa passar duas coisas para useCallback
:
- Uma definição de função que você deseja armazenar em cache entre re-renderizações.
- Uma lista de dependências incluindo cada valor dentro do seu componente que é usado dentro da sua função.
Na renderização inicial, a função retornada que você receberá do useCallback
será a função que você passou.
Nas renderizações seguintes, o React comparará as dependências com as dependências que você passou durante a renderização anterior. Se nenhuma das dependências mudou (comparada com Object.is
), o useCallback
retornará a mesma função que antes. Caso contrário, useCallback
retornará a função que você passou nesta renderização.
Em outras palavras, useCallback
armazena em cache uma função entre re-renderizações até que suas dependências mudem.
Vamos analisar um exemplo para ver quando isso é útil.
Digamos que você está passando uma função handleSubmit
do ProductPage
para o componente ShippingForm
:
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
Você notou que alternar a prop theme
faz o aplicativo travar por um momento, mas se você remover <ShippingForm />
do seu JSX, ele parece rápido. Isso indica que vale a pena tentar otimizar o componente ShippingForm
.
Por padrão, quando um componente re-renderiza, o React re-renderiza todos os seus filhos recursivamente. É por isso que, quando ProductPage
re-renderiza com um theme
diferente, o componente ShippingForm
também re-renderiza. Isso é aceitável para componentes que não exigem muito cálculo para re-renderizar. Mas se você verificou que uma re-renderização é lenta, pode avisar o ShippingForm
para pular a re-renderização quando suas props forem as mesmas da última renderização, envolvendo-o em memo
:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
Com essa mudança, ShippingForm
pulará a re-renderização se todas as suas props forem as mesmas da última renderização. É aqui que armazenar em cache uma função se torna importante! Vamos supor que você definiu handleSubmit
sem useCallback
:
function ProductPage({ productId, referrer, theme }) {
// Toda vez que o tema muda, esta será uma função diferente...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... assim as props do ShippingForm nunca serão as mesmas, e ele será re-renderizado toda vez */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Em JavaScript, uma function () {}
ou () => {}
sempre cria uma função diferente, semelhante a como o literal de objeto {}
sempre cria um novo objeto. Normalmente, isso não seria um problema, mas significa que as props do ShippingForm
nunca serão as mesmas, e sua otimização memo
não funcionará. É aqui que useCallback
é útil:
function ProductPage({ productId, referrer, theme }) {
// Diga ao React para armazenar em cache sua função entre re-renderizações...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...desde que essas dependências não mudem...
return (
<div className={theme}>
{/* ...ShippingForm receberá as mesmas props e pode pular a re-renderização */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Ao envolver handleSubmit
em useCallback
, você garante que é a mesma função entre as re-renderizações (até que as dependências mudem). Você não precisa envolver uma função em useCallback
a menos que faça isso por um motivo específico. Neste exemplo, o motivo é que você a passa para um componente envolto em memo
, e isso permite que ele pule a re-renderização. Existem outros motivos pelos quais você pode precisar de useCallback
, que são descritos mais adiante nesta página.
Deep Dive
Você verá muitas vezes useMemo
ao lado de useCallback
. Ambos são úteis quando você está tentando otimizar um componente filho. Eles permitem que você memoize (ou, em outras palavras, armazene em cache) algo que você está passando para baixo:
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Chama sua função e armazena seu resultado em cache
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Armazena em cache sua função
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
A diferença está em o que eles permitem que você armazene em cache:
useMemo
armazena em cache o resultado de chamar sua função. Neste exemplo, ele armazena em cache o resultado de chamarcomputeRequirements(product)
para que não mude a menos queproduct
tenha mudado. Isso permite que você passe o objetorequirements
sem re-renderizar desnecessariamente oShippingForm
. Quando necessário, o React chamará a função que você passou durante a renderização para calcular o resultado.- **
useCallback
armazena a própria função.” Diferente deuseMemo
, ele não chama a função que você fornece. Em vez disso, armazena a função que você forneceu para que a própriahandleSubmit
não mude a menos queproductId
oureferrer
tenham mudado. Isso permite que você passe a funçãohandleSubmit
sem re-renderizar desnecessariamente oShippingForm
. Seu código não será executado até que o usuário envie o formulário.
Se você já está familiarizado com useMemo
, pode achar útil pensar em useCallback
assim:
// Implementação simplificada (dentro do React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
Deep Dive
Se seu aplicativo for como este site, e a maioria das interações forem grosseiras (como substituir uma página ou uma seção inteira), a memoização geralmente é desnecessária. Por outro lado, se seu aplicativo se assemelhar mais a um editor de desenhos, e a maioria das interações forem granulares (como mover formas), então você pode achar a memoização muito útil.
Armazenar uma função em cache com useCallback
só é valioso em alguns casos:
- Você a passa como uma prop para um componente envolto em
memo
. Você deseja pular a re-renderização se o valor não mudou. A memoização permite que seu componente re-renderize apenas se as dependências mudaram. - A função que você está passando é usada posteriormente como uma dependência de algum Hook. Por exemplo, outra função envolta em
useCallback
depende dela, ou você depende dessa função douseEffect.
Não há benefício em envolver uma função em useCallback
em outros casos. Também não há dano significativo em fazer isso, então algumas equipes optam por não pensar sobre casos individuais e memoizar o máximo possível. O lado negativo é que o código se torna menos legível. Além disso, nem toda memoização é eficaz: um único valor que é “sempre novo” é suficiente para quebrar a memoização para um componente inteiro.
Observe que useCallback
não impede a criação da função. Você sempre está criando uma função (e isso é bom!), mas o React ignora isso e lhe dá de volta uma função em cache se nada mudou.
Na prática, você pode tornar a maioria das memoizações desnecessárias seguindo alguns princípios:
- Quando um componente embrulha visualmente outros componentes, deixe-o aceitar JSX como filhos. Assim, se o componente wrapper atualizar seu próprio estado, o React saberá que seus filhos não precisam re-renderizar.
- Prefira o estado local e não eleve o estado mais do que o necessário. Não mantenha estado transitório como formulários e se um item está sendo sobreposto no topo da sua árvore ou em uma biblioteca de estado global.
- Mantenha sua lógica de renderização pura. Se re-renderizar um componente causar um problema ou produzir algum artefato visual perceptível, isso é um erro no seu componente! Corrija o erro em vez de adicionar memoização.
- Evite Efeitos desnecessários que atualizam o estado. A maioria dos problemas de desempenho em aplicativos React é causada por cadeias de atualizações originadas de Efeitos que fazem seus componentes renderizarem repetidamente.
- Tente remover dependências desnecessárias dos seus Efeitos. Por exemplo, em vez de memoização, muitas vezes é mais simples mover algum objeto ou uma função para dentro de um Efeito ou fora do componente.
Se uma interação específica ainda parecer lenta, use a ferramenta de perfis do React Developer Tools para ver quais componentes se beneficiam mais da memoização e adicione memoização onde for necessário. Esses princípios tornam seus componentes mais fáceis de depurar e entender, portanto, é bom segui-los em qualquer caso. A longo prazo, estamos pesquisando fazer memoização automaticamente para resolver isso de uma vez por todas.
Example 1 of 2: Ignorando re-renderização com useCallback
e memo
Neste exemplo, o componente ShippingForm
é artificialmente desacelerado para que você possa ver o que acontece quando um componente React que você está renderizando é realmente lento. Tente incrementar o contador e alternar o tema.
Incrementar o contador parece lento porque força o desacelerado ShippingForm
a re-renderizar. Isso é esperado porque o contador mudou e, portanto, você precisa refletir a nova escolha do usuário na tela.
Em seguida, tente alternar o tema. Graças a useCallback
junto com memo
, é rápido apesar da desaceleração artificial! O ShippingForm
pulou a re-renderização porque a função handleSubmit
não mudou. A função handleSubmit
não mudou porque tanto productId
quanto referrer
(suas dependências do useCallback
) não mudaram desde a última renderização.
import { useCallback } from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); } function post(url, data) { // Imagine que isso envia uma solicitação... console.log('POST /' + url); console.log(data); }
Atualizando o estado a partir de um callback memoizado
Às vezes, você pode precisar atualizar o estado com base no estado anterior a partir de um callback memoizado.
Esta função handleAddTodo
especifica todos
como uma dependência porque ela calcula os próximos todos a partir dele:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
Normalmente, você desejará que funções memoizadas tenham o menor número possível de dependências. Quando você lê algum estado apenas para calcular o próximo estado, pode remover essa dependência passando uma função atualizadora em vez:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ Sem necessidade da dependência todos
// ...
Aqui, em vez de tornar todos
uma dependência e lê-lo por dentro, você passa uma instrução sobre como atualizar o estado (todos => [...todos, newTodo]
) para o React. Leia mais sobre funções atualizadoras.
Impedindo um efeito de disparar com muita frequência
Às vezes, você pode querer chamar uma função de dentro de um Efeito:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...
Isso cria um problema. Todo valor reativo deve ser declarado como uma dependência do seu Efeito. No entanto, se você declarar createOptions
como uma dependência, isso fará com que seu Efeito reconecte constantemente à sala de chat:
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problema: Esta dependência muda a cada renderização
// ...
Para resolver isso, você pode envolver a função que precisa chamar de um Efeito em useCallback
:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Muda apenas quando roomId muda
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Muda apenas quando createOptions muda
// ...
Isso garante que a função createOptions
seja a mesma entre as re-renderizações se o roomId
for o mesmo. No entanto, é ainda melhor remover a necessidade de uma dependência de função. Mova sua função para dentro do Efeito:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ Sem necessidade de useCallback ou dependências de função!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Muda apenas quando roomId muda
// ...
Agora seu código está mais simples e não precisa de useCallback
. Saiba mais sobre como remover dependências de Efeitos.
Otimizando um Hook personalizado
Se você estiver escrevendo um Hook personalizado, é recomendável envolver qualquer função que ele retorna em useCallback
:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
Isso garante que os consumidores do seu Hook possam otimizar seu próprio código quando necessário.
Solução de Problemas
Toda vez que meu componente renderiza, useCallback
retorna uma função diferente
Certifique-se de que você especificou a array de dependências como um segundo argumento!
Se você esquecer a array de dependências, useCallback
retornará uma nova função a cada vez:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Retorna uma nova função toda vez: sem array de dependências
// ...
Esta é a versão corrigida passando a array de dependências como um segundo argumento:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Não retorna uma nova função desnecessariamente
// ...
Se isso não ajudar, então o problema é que pelo menos uma de suas dependências é diferente da renderização anterior. Você pode depurar esse problema registrando manualmente suas dependências no console:
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
Você pode então clicar com o botão direito nas arrays de diferentes re-renderizações no console e selecionar “Armazenar como uma variável global” para ambas. Supondo que o primeiro tenha sido salvo como temp1
e o segundo como temp2
, você pode então usar o console do navegador para verificar se cada dependência nas duas arrays é a mesma:
Object.is(temp1[0], temp2[0]); // A primeira dependência é a mesma entre as arrays?
Object.is(temp1[1], temp2[1]); // A segunda dependência é a mesma entre as arrays?
Object.is(temp1[2], temp2[2]); // ... e assim por diante para cada dependência ...
Quando você descobrir qual dependência está quebrando a memoização, encontre uma maneira de removê-la ou memoize também.
Preciso chamar useCallback
para cada item da lista em um loop, mas não é permitido
Suponha que o componente Chart
esteja envolto em memo
. Você quer pular a re-renderização de cada Chart
na lista quando o componente ReportList
re-renderizar. No entanto, você não pode chamar useCallback
em um loop:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 Você não pode chamar useCallback em um loop assim:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
Em vez disso, extraia um componente para um item individual e coloque useCallback
lá:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Chame useCallback na raiz:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Alternativamente, você poderia remover useCallback
no último snippet e, em vez disso, envolver Report
em memo
. Se a prop item
não mudar, Report
pulará a re-renderização, então Chart
também pulará a re-renderização:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});