O useRef é um hook do react que recebe um initialValue como argumento e retorna uma referência, que é simplesmente um objeto com uma propriedade current. Quando se passa esse initialValue para o hook, a propriedade current já vem preenchida com esse valor. Abaixo está um exemplo desse uso simples:
import { useRef } from 'react';
function MyComponent() {
const initialValue = 0;
const reference = useRef(initialValue);
const someHandler = () => {
// Acessa o valor current
const value = reference.current;
// Atualiza o valor current
reference.current = newValue;
};
// ...
}
Tem 2 pontos importantes sobre essa referência:
O valor current é persistido durante as renderizações dos componentes. Ou seja, se você coloca o valor 2 no ref e o componente re-renderiza, o valor continua 2. Se você muda para 3 e o componente renderiza novamente, o valor continua 3.
Mudanças no current NÃO DISPARAM RENDERIZAÇÕES NOS COMPONENTES.
Por essas características, podemos usar o ref para persistir valores entre renderizações mas com o objetivo de que a mudança nesses valores não dispare nenhuma renderização desnecessária no componente.
Um exemplo prático para esse uso é na implementação de um timer:
function Stopwatch() {
const timerIdRef = useRef(0);
const [count, setCount] = useState(0);
const startHandler = () => {
if (timerIdRef.current) {
return;
}
timerIdRef.current = setInterval(() => setCount((c) => c + 1), 1000);
};
const stopHandler = () => {
clearInterval(timerIdRef.current);
timerIdRef.current = 0;
};
useEffect(() => {
return () => clearInterval(timerIdRef.current);
}, []);
return (
<div>
<div>Timer: {count}s</div>
<div>
<button onClick={startHandler}>Start</button>
<button onClick={stopHandler}>Stop</button>
</div>
</div>
);
}
Usamos timerIdRef para armazenar o setInterval. Percebamos também que não precisamos renderizar o componente toda vez que paramos ou começamos o timer, ou seja, toda vez que trocamos o setInterval. Com o ref, isso é possível. Se usássemos useState para armazenar o setInterval, todo start e stop causaria uma renderização no componente.
Manipulando DOM
Agora iremos para o caso mais comum para o dev front-end: A manipulação direta de elementos no DOM usando o useRef. Quando passamos o useRef num elemento, o React coloca a referência daquele elemento no current do ref, assim que o elemento é montado. Através dessa referência, podemos mudar o value, dar focus, dar play imperativamente em qualquer nó do DOM. Essa é a maneira correta de se manipular diretamente o DOM usando o React.
Usar métodos da api do DOM diretamente ( como o document.getElementById, document.getAlgo…) atrapalha na renderização do React, pois em alguns momentos a api do browser não notifica o React sobre as mudanças ocorridas, causando uma imprevisibilidade que não queremos! Vamos para um exemplo do useRef num elemento DOM:
import { useRef, useEffect } from 'react';
function InputFocus() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
No exemplo acima, estamos dando focus no input assim que o componente é renderizado pela primeira vez. Perceba que acessamos a api do input através do useRef. Você também pode usar a mesma ref para pegar o valor atual do input, acessando inputRef.current.value.
Podemos acessar o value do input com o ref por que os elementos de form do HTML tem um diferencial: Eles tem estado interno próprio. Por isso que não precisamos setar o value dele em nenhum momento, só pegar. Essa abordagem tem essa vantagem de performance e simplicidade, porém fica mais difícil monitorar mudanças nesse estado para aplicar validações, por exemplo. A título de curiosidade, essa abordagem é usada pela lib React hook form.
Typescript
No typescript sempre devemos declarar o tipo do elemento que queremos manipular. Isso vai evitar reclamações do TS e o Intellisense vai fornecer todos os métodos possíveis daquele elemento:
const focusNewCommentaryInputRef = useRef<HTMLInputElement | null>(null);
Uma ref apontada para o DOM sempre começa com null ou undefined, porque o React só a preenche depois de montar o componente. Portanto, é importante especificar isso no tipo para evitar erros.