useRef e useImperativeHandle
Desvendando o ref e forwardRef
Introdução
useRef
, createRef
, forwardRef
. Tanta coisa complicada e ainda tem o useImperativeHandle
pra complicar o que nós devemos usar. Mas a pergunta fica, quando eu devo usar ref?. Isso é uma pergunta que eu me faço pelo menos 2 vezes antes de querer criar uma ref em algum componente.
Antes de começar a explicar qualquer coisa, vamos listar alguns pontos:
- Pra quê serve um ref?
- Quando vou usar um ref?
- Quando devo usar forward Ref?
Antes de responder as perguntas, vou explicar cada um dos refs.
useRef | createRef
O useRef
serve para funções e o createRef
funciona para classes e o funcionamento de ambos é bem parecido. Podemos fazer a associação de useState
e this.setState
. Hooks e estado dos componentes de classe.
Mas o que são essas refs?
Resumidamente, refs nos dão a habilidade de criar objetos mutáveis que perduram durante todo o ciclo de vida do nosso componente. Em ambos os casos você acessará o objeto mutável por meio da property .current
. Com isso, podemos criar um valor em uma renderização e alterar durante qualquer parte do ciclo de vida, e o melhor de tudo, a ref não vai triggar um novo render no seu componente.
Refs não triggarem um novo render é algo bem interessante, pois com isso podemos armazenar valores e fazer modificações através de eventos. Dessa forma, evitamos renderizações na árvore do React e melhorar performance em casos de lentidão, ao custo de não ter um estado reativo a mudanças, somente aos eventos
E o que as refs tem a ver com o DOM?
Bem lembrado. Na documentação do React temos um tópico sobre isso e na maioria dos casos nós vemos exemplos com useRef
+ <input />
.
Por meio da property ref
contida nos nossos elementos HTML, podemos obter o HtmlElement
correspondente. Se você é da web antiga ou se já usou a DOM API, deve se lembrar do document.getElementByID
. E é exatamente o mesmo output que temos entre <div ref={ref} id="ok" />
e document.getElementById("ok")
. E como foi dito anteriormente, esse valor vai perdurar durante todo o ciclo de vida do nosso componente.
Pra ficar fixado na cabeça, um exemplo com hooks e outro com classes
const Component = () => {
const ref = useRef<HtmlDivElement>(null);
useEffect(() => {
if (!ref.current) return;
ref.current.style.backgroundColor = "black";
}, []);
return <div ref={ref}></div>;
};
Class Component
class Component extends React.PureComponent {
constructor(props) {
super(props);
this.divRef = React.createRef();
}
componentDidMount() {
this.divRef.current.style.backgroundColor = "black";
}
render() {
return <div ref={this.divRef}></div>;
}
}
{"React.forwardRef<Ref, Props>(props, externalRef)"}
De cara já temos a assinatura tipada + parâmetros do forwardRef. Mas para poder explicar o forwardRef
de forma tranquila, vamos voltar um pouco em como chamamos o nosso JSX e como nós criamos nossos componentes de função:
type Props = {
name: string;
};
const Component = (props: Props) => {
return <span>{props.name}</span>;
};
// Na hora de usarmos esse componente
<Component name="John Doe" />;
Como podemos ver, sempre é garantido que nosso componente vai receber um objeto via props. Logo, para usar as refs basta nós passarmos um ref para o componente e iremos capturar as refs dele.
// Seguindo o exemplo acima
const Main = () => {
const ref = useRef();
<Component ref={ref} name="John Doe" />;
};
Tudo certo? NÃOOOOOOOOOOOOOOOOO. De repente, brotou um mega erro no console e nós estamos perdidos sobre como usar a ref. Se você tentar acessar a ref de um componente sem o forwardRef
, você verá o seguinte erro:
Erro ao usar ref sem forward ref
Warning: Function components cannot be given refs.
Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Bom, com isso nós podemos ir lá na documentação do React e entender como fazemos para passar as refs do nosso componente para o componente pai (o componente que o chama). Caso você não queira ir, vamos fazer isso por aqui.
type Props = {
name: string;
};
const Component = forwardRef<HTMLSpanElement, Props>((props: Props, ref) => {
return <span ref={ref}>{props.name}</span>;
});
Agora sim. Podemos utilizar a property ref
ao usar o nosso Component
e não haverão mais erros. E o mais legal? Quem consumir este componente vai ter o tipo exato da ref, sem nenhum problema na hora de consumir
Funcionamento da ref do Component <span />
Se você fizer um document.querySelector("span")
ou qualquer outro método de acesso ao DOM que vá retornar um span, verá que as properties são as mesmas. E ainda mais, se você fizer um Object.is(ref.current, document.querySelector("span"))
eles serão os mesmos objetos.
Se você não conhece o Object.is, aqui vai uma indicação da MDN.
Voltando um pouco a construção dos componentes, estamos habituados a componentes de função aceitarem somente um único argumento, que são nossas props. Mas com o forwardRef nós recebemos um segundo parâmetro que é a nossa ref externa, que o pai do componente irá passar para o filho.
Dessa forma apresentada, nós só conseguimos fazer um forward da ref de um elemento do HTML.
E se eu quiser criar meu próprio objeto de ref, como eu faço?
Ótima pergunta
useImperativeHandle
Aaah, os hooks...Como eles facilitam nossa vida. Este aqui eu deixei por último pois para usar o useImperativeHandle
você precisa do forwardRef
. Nosso array de dependências pra esse hook já está preenchido, agora só aprender
import { useImperativeHandle, forwardRef } from "react";
type Ref = {
changeColor: () => void;
scrollTo: () => void;
};
type Props = {
name: string;
};
const Component = forwardRef<Ref, Props>((props, externalRef) => {
const ref = useRef<HtmlDivElement>(null);
useImperativeHandle(externalRef, () => {
return {
changeColor: () =>
(ref.current?.style.backgroundColor =
// https://stackoverflow.com/questions/1484506/random-color-generator
"#" + (((1 << 24) * Math.random()) | 0).toString(16)),
scrollTo: () => ref.current?.scrollIntoView(),
};
});
return <div ref={ref}>Hack The planet</div>;
});
Apesar de ser um conceito complexo, o uso é bem simples. Existem alguns tradeoffs no useImperativeHandle que são referentes a objetos, uma vez que você tem uma instância mutável, você trás todos os conceitos que já conhece nos objetos.
Com o useImperativeHandle
você irá criar métodos ou atributos do seu componente filho que irá refletir no pai, sem gerar nenhum novo render. E essa é uma forma de passar props do filho para o pai.
Respondendo as perguntas
Como eu tinha levantado 3 perguntas no começo do artigo, iremos respondê-las agora para chegar a conclusão
Pra quê serve um ref?
R: Acessar o elemento DOM ou criar valores mutáveis que irão perdurar durante todo o ciclo de vida do componente
Quando vou usar um ref?
R: Quando quiser acessar o HtmlElement de um elemento ou quiser receber as refs de um componente filho
Quando devo usar forward Ref?
R: Quando estiver criando um componente que será a abstração de um elemento HTML ou quando quiser fornecer métodos do filho para o pai
Conclusão
Refs são uma verdadeira mágica que nos permite trabalhar diretamente com o DOM, de forma imperativa. Isso pode ser muito útil em alguns casos onde você cria uma biblioteca que faça diversas mudanças diretas no DOM.
Apesar dessa mágica toda, usar o Ref pode ser um tiro pela culatra e acabar gerando problemas, uma vez que você fará mudanças diretas no DOM e o React irá fazer mudanças no Shadow DOM para posteriormente aplicar as mudanças. Seria mais ou menos um efeito de fazer 2 setStates ao mesmo tempo.
Espero que vocês tenham curtido e entendido como funcionam as refs e como fazer para transitar as refs entre componentes. E isso é tudo pessoal.