Durante o desenvolvimento, frequentemente precisamos interagir diretamente com os elementos do DOM. Nesses casos, o React nos fornece um mecanismo chamado refs, que permite acesso aos elementos depois de serem renderizados. Comumente, usamos refs de objeto padrão via useRef
(vamos chamá-los assim), mas há outra abordagem conhecida como callback refs. Este método oferece flexibilidade e controle adicionais sobre o ciclo de vida dos elementos, nos permitindo realizar ações específicas em momentos precisos quando os elementos são anexados ou desanexados do DOM.
Neste artigo, quero explicar o que são os callback refs e como eles funcionam, discutir os problemas que você pode encontrar e mostrar exemplos de seu uso.
O que São Callback Refs e Como Eles Funcionam?
Callback refs oferecem a você um controle mais granular sobre a anexação de ref em comparação com refs de objeto. Vamos ver como eles funcionam na prática:
- Montagem. Quando um elemento é montado no DOM, React chama a função de ref com o próprio elemento DOM. Isso permite que você execute ações com o elemento imediatamente após ele aparecer na página.
- Desmontagem. Quando um elemento é desmontado, o React chama a função de ref com
null
. Isso lhe dá a oportunidade de limpar ou cancelar quaisquer ações associadas a esse elemento.
Exemplo: Rastreando Montagem e Desmontagem
import React, { useCallback, useState } from 'react';
function MountUnmountTracker() {
const [isVisible, setIsVisible] = useState(false);
const handleRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
console.log('Element mounted:', node);
} else {
console.log('Element unmounted');
}
}, []);
return (
<div>
<button onClick={() => setIsVisible((prev) => !prev)}>
{isVisible ? 'Hide' : 'Show'} element
</button>
{isVisible && <div ref={handleRef}>Tracked element</div>}
</div>
);
}
export default MountUnmountTracker;
Cada vez que alternamos a visibilidade do elemento, a função handleRef
é chamada com o nó ou null
, permitindo-nos rastrear o momento em que o elemento é anexado ou removido.
Problemas Comuns e Soluções
Problema: Invocações Repetidas do Callback Ref
Um problema frequente ao usar callback refs é a criação repetida da função de ref em cada re-renderização do componente. Por causa disso, o React pensa que é um novo ref, chama o antigo com null
(limpando-o) e então inicializa o novo — mesmo que nosso elemento ou componente não tenha realmente mudado. Isso pode levar a efeitos colaterais indesejados.
Exemplo do Problema
Considere um componente Básico
que tem um botão para alternar a visibilidade de um div
com um callback ref, além de outro botão para forçar uma re-renderização do componente:
import React, { useState, useReducer } from 'react';
function Basic() {
const [showDiv, setShowDiv] = useState(false);
const [, forceRerender] = useReducer((v) => v + 1, 0);
const toggleDiv = () => setShowDiv((prev) => !prev);
const refCallback = (node: HTMLDivElement | null) => {
console.log('div', node);
};
return (
<div>
<button onClick={toggleDiv}>Toggle Div</button>
<button onClick={forceRerender}>Rerender</button>
{showDiv && <div ref={refCallback}>Example div</div>}
</div>
);
}
export default Basic;
Cada vez que você clica no botão Rerender, o componente é re-renderizado, criando uma nova função refCallback
. Como resultado, o React chama o antigo refCallback(null)
e então o novo refCallback(nó)
, mesmo que nosso elemento com o ref não tenha mudado. No console, você verá div null
e então div [nó]
sucessivamente. Obviamente, geralmente queremos evitar chamadas desnecessárias como essas.
Solução: Memorizando o Callback Ref Com useCallback
Evitar isso é bastante simples: basta usar useCallback
para memorizar a função. Dessa forma, a função permanece inalterada através das re-renderizações, a menos que suas dependências mudem.
import React, { useState, useCallback, useReducer } from 'react';
function Basic() {
const [showDiv, setShowDiv] = useState(false);
const [, forceRerender] = useReducer((v) => v + 1, 0);
const toggleDiv = () => setShowDiv((prev) => !prev);
const refCallback = useCallback((node: HTMLDivElement | null) => {
console.log('div', node);
}, []);
return (
<div>
<button onClick={toggleDiv}>Toggle Div</button>
<button onClick={forceRerender}>Rerender</button>
{showDiv && <div ref={refCallback}>Example div</div>}
</div>
);
}
export default Basic;
Agora, refCallback
é criado apenas uma vez, na renderização inicial. Não irá desencadear chamadas extras em renderizações posteriores, evitando callbacks desnecessários e melhorando o desempenho.
A Ordem dos Callback Refs, useLayoutEffect e useEffect
Antes de falarmos sobre como usar callback refs em seu código para resolver problemas específicos, vamos entender como os callback refs interagem com os hooks useEffect
e useLayoutEffect
para que você possa organizar adequadamente a inicialização e limpeza de recursos.
Ordem de Execução
- callback ref – chamado imediatamente após renderizar os elementos DOM, antes dos hooks de efeito serem executados
- useLayoutEffect – é executado após todas as mutações no DOM, mas antes do navegador pintar
- useEffect – é executado após o componente ter terminado de renderizar na tela
import React, { useEffect, useLayoutEffect, useCallback } from 'react';
function WhenCalled() {
const refCallback = useCallback((node: HTMLDivElement | null) => {
if (node) {
console.log('Callback ref called for div:', node);
} else {
console.log('Callback ref detached div');
}
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect called');
}, []);
useEffect(() => {
console.log('useEffect called');
}, []);
return (
<div>
<div ref={refCallback}>Element to watch</div>
</div>
);
}
export default WhenCalled;
Saída no Console
Callback ref chamado para div: [elemento div]
useLayoutEffect chamado
useEffect chamado
Esta sequência nos diz que os callback refs são acionados antes dos hooks como useLayoutEffect
e useEffect
, o que é essencial ter em mente ao escrever sua lógica.
Quais Problemas os Callback Refs Resolvem no Código?
Primeiro, vamos reproduzir um problema geralmente encontrado com refs de objeto regulares para então resolvê-lo com callback refs.
import { useCallback, useEffect, useRef, useState } from 'react';
interface ResizeObserverOptions {
elemRef: React.RefObject<HTMLElement>;
onResize: ResizeObserverCallback;
}
function useResizeObserver({ elemRef, onResize }: ResizeObserverOptions) {
useEffect(() => {
const element = elemRef.current;
if (!element) {
return;
}
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(element);
return () => {
resizeObserver.unobserve(element);
};
}, [onResize, elemRef]);
}
export function UsageDom() {
const [bool, setBool] = useState(false);
const elemRef = useRef<HTMLDivElement>(null);
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
console.log('resize', entries);
}, []);
useResizeObserver({ elemRef, onResize: handleResize });
const renderTestText = () => {
if (bool) {
return <p ref={elemRef}>Test text</p>;
}
return <div ref={elemRef}>Test div</div>;
};
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<button onClick={() => setBool((v) => !v)}>Toggle</button>
{renderTestText()}
</div>
);
}
Não vamos nos aprofundar em cada detalhe aqui. Em resumo, estamos monitorando o tamanho do nosso elemento div
ou p
através de um ResizeObserver
. Inicialmente, tudo funciona bem: ao montar, conseguimos obter o tamanho do elemento, e os redimensionamentos também são relatados no console.
O verdadeiro problema começa quando alternamos o estado para mudar o elemento que estamos observando. Quando mudamos o estado e, portanto, substituímos o elemento monitorado, nosso ResizeObserver
não funciona mais corretamente. Ele continua observando o primeiro elemento, que já foi removido do DOM! Mesmo alternar de volta para o elemento original não ajuda porque a assinatura do novo elemento nunca se conecta corretamente.
Nota: A solução a seguir é mais representativa do que você poderia escrever em um contexto de biblioteca, precisando de código universal. Em um projeto real, você poderia resolver isso através de uma combinação de flags, efeitos, etc. Mas em código de biblioteca, você não tem conhecimento do componente específico ou do seu estado. Este é exatamente o cenário onde referências de callback podem nos ajudar.
import { useCallback, useRef, useState } from 'react';
function useResizeObserver(onResize: ResizeObserverCallback) {
const roRef = useRef<ResizeObserver | null>(null);
const attachResizeObserver = useCallback(
(element: HTMLElement) => {
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(element);
roRef.current = resizeObserver;
},
[onResize]
);
const detachResizeObserver = useCallback(() => {
roRef.current?.disconnect();
}, []);
const refCb = useCallback(
(element: HTMLElement | null) => {
if (element) {
attachResizeObserver(element);
} else {
detachResizeObserver();
}
},
[attachResizeObserver, detachResizeObserver]
);
return refCb;
}
export default function App() {
const [bool, setBool] = useState(false);
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
console.log('resize', entries);
}, []);
const resizeRef = useResizeObserver(handleResize);
const renderTestText = () => {
if (bool) {
return <p ref={resizeRef}>Test text</p>;
}
return <div ref={resizeRef}>Test div</div>;
};
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<button onClick={() => setBool((v) => !v)}>Toggle</button>
{renderTestText()}
</div>
);
}
Como você pode ver, reescrevemos nosso hook useResizeObserver
para usar uma referência de callback. Nós apenas passamos uma callback (memorizada) que deve ser acionada no redimensionamento, e não importa quantas vezes alternamos elementos, nosso callback de redimensionamento ainda funciona. Isso acontece porque o observador se conecta a novos elementos e se desconecta de antigos exatamente no momento que queremos, graças à referência de callback.
O principal benefício aqui é que o desenvolvedor que usa nosso gancho não precisa mais se preocupar com a lógica de adicionar/remover observadores nos bastidores – encapsulamos essa lógica dentro do gancho. O desenvolvedor só precisa passar um retorno de chamada para o nosso gancho e anexar sua referência retornada aos elementos.
Combinando Múltiplas Refs em Uma
Aqui está outro cenário onde as refs de retorno de chamada entram em ação:
import { useEffect, useRef } from 'react';
import { forwardRef, useCallback } from 'react';
interface InputProps {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
const Input = forwardRef(function Input(
props: InputProps,
ref: React.ForwardedRef<HTMLInputElement>
) {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (!inputRef.current) {
return;
}
console.log(inputRef.current.getBoundingClientRect());
}, []);
return <input {...props} ref={ref} />;
});
export function UsageWithoutCombine() {
const inputRef = useRef<HTMLInputElement | null>(null);
const focus = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} />
<button onClick={focus}>Focus</button>
</div>
);
}
No código acima, temos um componente de input simples no qual definimos uma ref, pegando-a das props usando forwardRef
. Mas como usamos inputRef
dentro do componente Input
se também precisamos da ref de fora para algo como focar? Talvez queiramos fazer algo mais no próprio componente de input, como getBoundingClientRect
. Substituir a ref da propriedade pela nossa ref interna significa que o foco de fora não funcionará mais. Então, como combinamos essas duas refs?
Aqui é onde as refs de retorno de chamada ajudam novamente:
import { useEffect, useRef } from 'react';
import { forwardRef, useCallback } from 'react';
type RefItem<T> =
| ((element: T | null) => void)
| React.MutableRefObject<T | null>
| null
| undefined;
function useCombinedRef<T>(...refs: RefItem<T>[]) {
const refCb = useCallback((element: T | null) => {
refs.forEach((ref) => {
if (!ref) {
return;
}
if (typeof ref === 'function') {
ref(element);
} else {
ref.current = element;
}
});
}, refs);
return refCb;
}
interface InputProps {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
const Input = forwardRef(function Input(
props: InputProps,
ref: React.ForwardedRef<HTMLInputElement>
) {
const inputRef = useRef<HTMLInputElement>(null);
const combinedInputRef = useCombinedRef(ref, inputRef);
useEffect(() => {
if (!inputRef.current) {
return;
}
console.log(inputRef.current.getBoundingClientRect());
}, []);
return <input {...props} ref={combinedInputRef} />;
});
export function UsageWithCombine() {
const inputRef = useRef<HTMLInputElement | null>(null);
const focus = () => {
inputRef.current?.focus();
};
return (
<div>
<Input ref={inputRef} />
<button onClick={focus}>Focus</button>
</div>
);
}
Explicação
Implementamos um gancho useCombinedRef
onde um desenvolvedor pode passar refs padrão, refs de retorno de chamada, e opcionalmente null
ou undefined
. O gancho em si é apenas um useCallback
que percorre o array de refs. Se o argumento for null
, o ignoramos, mas se a ref for uma função, a chamamos com o elemento; se for um objeto de ref padrão, definimos ref.current
para o elemento.
Dessa forma, mesclamos múltiplas referências em uma só. No exemplo acima, tanto getBoundingClientRect
dentro do componente Input
quanto a chamada de foco externa funcionarão corretamente.
O que Mudou no React 19 em Relação às Referências de Callback?
Limpeza Automática
O React agora lida automaticamente com a limpeza de referências de callback quando os elementos são desmontados, tornando o gerenciamento de recursos mais simples. Aqui está um exemplo que a equipe do React mostra em sua documentação:
{
// referência criada
// NOVO: retorne uma função de limpeza para redefinir
// a referência quando o elemento é removido do DOM.
return () => {
// limpeza da referência
};
}}
/>
Você pode ler mais detalhes sobre isso no post oficial do blog, onde é mencionado que em breve, limpar referências via null
pode ser descontinuado, deixando uma única maneira padronizada de limpar referências.
Escolhendo Entre Referências Normais e Referências de Callback
- Use referências padrão (
useRef
) quando você só precisar de acesso simples a um elemento DOM ou quiser preservar algum valor entre renderizações sem ações adicionais ao anexar ou desanexar. - Use referências de callback quando você precisar de um controle mais granular sobre o ciclo de vida do elemento, quando estiver escrevendo código universal (por exemplo, na sua própria biblioteca ou pacote), ou quando precisar gerenciar múltiplas referências juntas.
Conclusão
Referências de callback no React são uma ferramenta poderosa que oferece aos desenvolvedores flexibilidade e controle adicionais ao trabalhar com elementos DOM. Na maioria dos casos, referências de objeto padrão via useRef
são suficientes para tarefas do dia a dia, mas referências de callback podem ajudar em cenários mais complexos, como os que discutimos acima.
Source:
https://dzone.com/articles/react-callback-refs-guide