Evitando useEffect com callback refs

Interação com um elemento DOM não precisa necessariamente do useEffect

Este artigo é uma tradução de Avoiding useEffect with callback refs

Originalmente escrito em inglês por @TkDodo


Obs: Este artigo assume que você tenha um entendimento básico do que são refs no React.

Apesar de os refs serem mutáveis aonde teoricamente podemos salvar valores arbitrários, eles são frequentemente mais usados para ter acesso a um nó do DOM:

const ref = React.useRef(null)

return <input ref={ref} defaultValue="Hello world" />

ref é uma propriedade reservada embutida nos primitivos aonde o React irá salvar o nó do DOM depois de renderizado. Ele será definido de volta para null quando o componente for desmontado.

Interagindo com refs

Para a maioria das interações, você não precisa acessar o elemento subjacente, porque o React lidará com as atualizações para nós automaticamente. Um bom exemplo é aonde você pode precisar de um ref é gerenciar o foco.

Há um bom RFC do Devon Govett que propõe adicionar um Gerenciador de Foco ao rect-dom, mas agora não há nada no React que nós ajude com isso.

Foco com um effect

Então, agora, como você, focaria um elemento de input depois de renderizado? (Eu sei autofocus existe, isto é um exemplo. Se isso lhe incomoda imagino que você gistaria de animar o nó.)

Bom, a maioria dos códigos que eu vi tenta fazer isso:

const ref = React.useRef(null)

React.useEffect(() => {
  ref.current?.focus()
}, [])

return <input ref={ref} defaultValue="Hello world" />

Isso é a principio bom e não viola nenhuma regra. O array de dependências vazios é ok porque a única coisa usada dentro é o ref que é estável. O linter não irá reclamar de adicionar ao array de dependencia, e o ref também não será lido durante a redenrização (o que pode ser problemático com recursos simultaneos do React)

O effect será executado uma vez "na montagem" (duas vezes em strict mode). A essa altura React já preencheu o ref com o nó do DOM então podemos dar foco nele.

Sim está não é o melhor maneira de se fazer isso e tem algumas ressalvas em algumas situações mais avançadas.

Especificamente ele assume que o ref será "preenchido" quando o effect for executado. Se não estiver disponível, por ex. quando você passa o ref para um componente customizado que irá adiar a renderização ou somente mostrar o input depois de alguma outra interação do usuário, o conteúdo do ref continuará null e quando o effect ser executado nada será focado:

function App() {
  const ref = React.useRef(null)

  React.useEffect(() => {
    // 🚨 ref.current será sempre será null quando executado
    ref.current?.focus()
  }, [])

  return <Form ref={ref} />
}

const Form = React.forwardRef((props, ref) => {
  const [show, setShow] = React.useState(false)

  return (
    <form>
      <button type="button" onClick={() => setShow(true)}>
        show
      </button>
      // 🧐 ref esta anexado ao input, mas é renderizado condicionalmente
      // então isso não será preenchido quando o effect acima for executar
      {show && <input ref={ref} />}
    </form>
  )
})

Aqui está o que acontece:

  • Formulário renderiza.
  • input não é renderizado, ref continua null
  • effect executa, nada acontece
  • input é mostrado, ref será preenchido mas não será focado porque o effect não será executado novamente.

O problema é que o effect é "vinculado" a função renderização do Formulário, enquanto na verdade queremos que: "Foca o input quando ele for renderizado", não "quando o formulário é montado".

Callback refs

É aqui que os callbacks refs entram in jogo. Se você já olhou para declarações de tipo para refs, podemos ver que podemos não só passar um objeto ref para ele, mas também uma função:

type Ref<T> = RefCallback<T> | RefObject<T> | null

Conceitualmente eu gosto de pensar em refs em elementos do React como funções que são chamadas depois que o componente é renderizado. Está função obtem o elemento DOM renderizado passado como argumento. Se o elemento React for desmonta ele será chamado mais uma vez com null. Passando um ref useRef (um RefObject) para um elemento React é Syntactic Sugar (mamão com açucar):

<input
  ref={(node) => {
    ref.current = node;
  }}
  defaultValue="Hello world"
/>

Deixe-me enfatizar isso mais uma vez: Todas propriedades ref são apenas funções!

E essas funções executam depois da renderização, aonde não há problema executar efeitos colaterais. Talvez fosse melhor se o ref fosse somente chamado depoisDaRenderizacao ou algo assim.

Com esse conhecimento, o que nós impede de de forcar o input dentro do callback ref, aonde temos acesso direto ao nó?

<input
  ref={(node) => {
    node?.focus()
  }}
  defaultValue="Hello world"
/>

Bom, um pequeno detalhe: React irá executar essa função após toda renderização. Então, ao menos que estejamos bem em focar nosso input com tanta frequência (o que, provavelmente, não estamos), temos que dizer para o React somente executar isso somente quando quisermos.

useCallback para o resgate

Felizmente, o React usa estabilidade referencial para verificar se o callback ref deve ser executado ou não. Isso significa que se passarmos o mesmo ref para ele, a execução será ignorada.

E é que entra o useCallback, porque é assim que garantimos que a função não seja criada desnecessariamente. Talvez por isso que eles são chamados de callback-refs - porque você tem que envolvelos em useCallback o tempo todo. 😂

Aqui está a solução final:

const ref = React.useCallback((node) => {
  node?.focus()
}, [])

return <input ref={ref} defaultValue="Hello world" />

Comparando com a versão inicial, é menos código e usa apenas um hook no lugar de dois, Além disso, funcionará em todas situações, poque o callback ref está vinculado ao ciclo de vida do nó DOM, não do componente que o monta. Além disso ele não será executado duas vezes no strict mode (quando executado no ambiente de desenvolvimento) o que parece ser importante para muitos.

E como mostrado nessa joia escondida na (antiga) documentação do React, você pode usar isso para executar qualquer tipo de efeito colateral, ex. chamar setState nele. Eu vou deixar um exemplo aqui poque na verdade é muito bom:

function MeasureExample() {
  const [height, setHeight] = React.useState(0)

  const measuredRef = React.useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height)
    }
  }, [])

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  )
}

Então por favor, se você precisa interagir com elementos DOM diretamente depois da renderização, tente não pular direto no useRef + useEffect, mas considere usar callback refs em vez disso.

Original article in English can be read at Avoiding useEffect with callback refs