useRef e useImperativeHandle

Desvendando o ref e forwardRef

Table of Content

    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 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.