Durante o desenvolvimento, frequentemente precisamos interagir diretamente com elementos do DOM. Em tais casos, o React nos fornece um mecanismo chamado refs, que permite o acesso aos elementos após eles terem sido renderizados. Mais comumente, usamos refs de objeto padrão via useRef
(vamos chamá-los assim), mas há outra abordagem conhecida como refs de callback. Este método oferece flexibilidade adicional e controle sobre o ciclo de vida dos elementos, nos permitindo realizar ações específicas em momentos precisos em que os elementos são anexados ou desanexados do DOM.
Neste artigo, quero explicar o que são refs de callback e como eles funcionam, discutir as armadilhas que você pode encontrar e mostrar exemplos de seu uso.
O que são refs de callback e como eles funcionam?
Refs de callback oferecem a você um controle mais granular sobre a anexação de refs em comparação com refs de objeto. Vamos ver como eles funcionam na prática:
- Montagem. Quando um elemento é montado no DOM, o React chama a função ref com o próprio elemento do 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 ref com
null
. Isso lhe dá a oportunidade de limpar ou cancelar quaisquer ações associadas a esse elemento.
Exemplo: Rastreando a 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 desanexado.
Problemas Comuns e Soluções
Problema: Invocações Repetidas de Ref de Callback
Um problema frequente ao usar refs de callback é a criação repetida da função de ref em cada nova renderização do componente. Por causa disso, o React entende 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 ref de callback, além de outro botão para forçar uma nova 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 Renderizar Novamente, o componente é renderizado novamente, criando uma nova função refCallback
. Como resultado, o React chama o antigo refCallback(null)
e então o novo refCallback(node)
, 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 essa.
Solução: Memorizando o Ref de Callback Com useCallback
A evitar isso é bastante simples: basta usar useCallback
para memorizar a função. Dessa forma, a função permanece inalterada entre as 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 acionará chamadas extras em renderizações subsequentes, 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 a renderização dos 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 terminar de ser renderizado 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
Essa sequência nos diz que os callback refs são acionados antes dos hooks como useLayoutEffect
e useEffect
, o que é essencial lembrar 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 depois 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 entrar em todos os detalhes aqui. Em resumo, estamos rastreando o tamanho do nosso elemento div
ou p
através de um ResizeObserver
. Inicialmente, tudo funciona bem: ao montar, podemos obter o tamanho do elemento e as redimensionamentos também são relatados no console.
O verdadeiro problema começa quando alternamos o estado para trocar o elemento que estamos observando. Quando mudamos o estado e, portanto, substituímos o elemento rastreado, nosso ResizeObserver
não funciona mais corretamente. Ele continua observando o primeiro elemento, que já foi removido do DOM! Mesmo alternando de volta para o elemento original não ajuda porque a inscrição no novo elemento nunca é anexada corretamente.
Nota: A solução a seguir é mais representativa do que você poderia escrever em um contexto de biblioteca, necessitando de código universal. Em um projeto real, você pode resolvê-lo através de uma combinação de flags, efeitos, etc. Mas no código da biblioteca, você não tem conhecimento do componente específico ou de seu estado. Este é exatamente o cenário em que as referências de retorno de chamada 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 gancho useResizeObserver
para usar uma referência de retorno de chamada. Apenas passamos um retorno de chamada (memorizado) que deve ser acionado no redimensionamento, e não importa quantas vezes alternemos os elementos, nosso retorno de chamada de redimensionamento ainda funciona. Isso ocorre porque o observador se anexa aos novos elementos e se desanexa dos antigos no momento exato em que desejamos, cortesia da referência de retorno de chamada.
O principal benefício aqui é que o desenvolvedor que utiliza 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 seus elementos.
Combinando Múltiplas Refs Em Uma Só
Aqui está outro cenário em que as refs de retorno de chamada vêm para o resgate:
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 entrada simples no qual definimos uma ref, obtendo-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 entrada, 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 próprio gancho é apenas um useCallback
que itera sobre 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
como o elemento.
Dessa forma, fundimos várias refs em uma só. No exemplo acima, tanto getBoundingClientRect
dentro do componente Input
, quanto a chamada externa de foco funcionarão corretamente.
O que mudou no React 19 em relação às refs de callback?
Limpeza Automática
O React agora lida automaticamente com a limpeza das refs de callback quando os elementos são desmontados, tornando a gestão de recursos mais simples. Aqui está um exemplo que a equipe do React mostra em sua documentação:
{
// ref criada
// NOVO: retorna uma função de limpeza para redefinir
// a ref quando o elemento é removido do DOM.
return () => {
// limpeza da ref
};
}}
/>
Você pode ler mais detalhes sobre isso no post oficial do blog, onde é mencionado que em breve, a limpeza de refs via null
pode ser descontinuada, deixando apenas uma maneira padronizada de limpar as refs.
Escolhendo Entre Refs Normais e Refs de Callback
- Use refs padrão (
useRef
) quando você apenas precisa de acesso simples a um elemento DOM ou deseja preservar algum valor entre as renderizações sem ações adicionais de anexar ou desanexar. - Use refs de callback quando você precisa de um controle mais granular sobre o ciclo de vida do elemento, quando está escrevendo um código universal (por exemplo, em sua própria biblioteca ou pacote) ou quando precisa gerenciar várias refs juntas.
Conclusão
Referências de callback no React são uma ferramenta poderosa que oferece aos desenvolvedores flexibilidade e controle extra ao trabalhar com elementos DOM. Na maioria dos casos, referências de objeto padrão via useRef
são suficientes para tarefas cotidianas, 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