Referenciando valores com Refs

Quando você quer que um componente “lembre” de alguma informação, mas você não quer que aquela informação cause novos renders, você pode usar um ref.

Você aprenderá

  • Como adicionar um ref ao seu componente
  • Como atualizar o valor de um ref
  • Como refs diferenciam de state
  • Como usar refs de forma segura

Adicionando um ref ao seu componente

Você pode adicionar um ref ao seu componente importando o Hook useRef do React:

import { useRef } from 'react';

Dentro do seu componente, invoque o Hook useRef e passe o valor inicial que você quer referenciar como o único argumento. Por exemplo, aqui está um ref para o valor 0:

const ref = useRef(0);

useRef retorna um objeto assim:

{
current: 0 // o valor que você passou para o useRef
}
Uma flecha com os escritos 'current', dentro de um bolso com os escritos 'ref'.

Illustrated by Rachel Lee Nabors

Você pode acessar o valor atual daquele ref através da propriedade ref.current. Esse valor é intencionalmente mutável, o que significa que você pode tanto ler quanto escrever sobre ele. É como um bolso secreto do seu componente o qual o React não rastreia. (É isso que o faz uma “saída de emergência” do fluxo de data de mão-única do React—mais sobre isso abaixo!)

Aqui, um botão irá incrementar ref.current a cada clique:

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

O ref aponta para um número, mas, como state, você pode apontá-lo para qualquer coisa: uma string, um objeto, ou até mesmo uma função. Diferentemente do state, ref é um simples objeto Javascript com a propriedade current que você pode ler e modificar.

Note que o componente não re-renderiza com cada incremento. Assim como state, refs são retidos pelo React entre re-renderizações. Entretanto, alterar o state re-renderiza um componente. Mudar um ref não!

Exemplo: construindo um cronômetro

Você pode combinar refs e state em um único componente. Por exemplo, vamos fazer um cronômetro que o usuário possa iniciar ou parar ao pressionar um botão. Para exibir quanto tempo passou desde que o usuário pressionou “Start”, você precisará rastrear quando o botão Start foi pressionado e qual o horário atual. Essas informações são usadas para renderização, então as manteremos no state:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

Quando o usuário pressionar “Start”, você usará setInterval para atualizar o tempo a cada 10 milissegundos:

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Inicia contagem.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Atualizar o tempo atual a cada 10 milissegundos.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

Quando o botão “Stop” é pressionado, você precisará cancelar o intervalo existente de forma que ele pare de atualizar a variável now do state.Você pode fazer isso invocando ‘clearInterval’, mas você precisará passar o ID do intervalo que foi retornado anteriormente pela invocação do setInterval quando o usuário pressionou Start. Você precisará gravar esse ID do intervalo em algum lugar. Já que o ID do intervalo não é usado para renderização, você pode guardá-lo em um ref:

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

Quando uma informação é usada para renderização, mantenha-a no state. Quando uma informação é necessária somente por manipuladores de eventos (event handlers) e mudá-la não requer uma re-renderização, usar um ref pode ser mais eficiente.

Diferenças entre refs e state

Talvez você esteja pensando que refs parecem ser menos “rigorosos” que state—você pode mutá-los ao invés de sempre ter que usar uma função de definir state, por exemplo. Mas na maioria dos casos, você irá querer usar state. Refs são uma “válvula de escape” que você não precisará com frequência. Aqui uma comparação entre state e refs:

refsstate
useRef(initialValue) retorna { current: initialValue }useState(initialValue) retorna o valor atual de uma variável de state e uma função setter do state ( [value, setValue])
Não provoca re-renderização quando alterada.Provoca re-renderização quando alterada.
Mutável—você pode modificar e atualizar o valor de current de fora do processo de renderização.”Imutável”—você deve usar a função de definir state para modificar variáveis de state e despachar uma re-renderização.
Você não deve ler (ou sobrescrever) o valor de current durante uma rerenderização.Você pode ler state a qualquer momento. Entretanto, cada renderização tem seu snapshot do state o qual não muda.

Aqui um botão contador que foi implementado com state:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>
  );
}

Como o valor count é exibido, faz sentido usar um valor de state para ele. Quando o valor do contador é definido com setCount(), React re-renderiza o componente e a tela é atualizada para refletir o novo valor.

Se você tentasse implementar isso com um ref, React nunca re-renderizaria o componente, então você nunca veria o contador mudar! Veja como, ao clicar neste botão, seu texto não é atualizado:

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // Isso não re-renderiza o componente!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      You clicked {countRef.current} times
    </button>
  );
}

É por isso que ler ref.current durante a renderização leva a um código não confiável. Se você precisar disso, dê preferência ao state.

Deep Dive

como useRef funciona por dentro?

Apesar de ambos useState e useRef serem providos pelo React, em princípio useRef poderia ser implementado em cima de useState. Você pode imaginar que dentro do React, useRef é implementado assim:

// Dentro do React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}

Durante a primeira renderização, useRef retorna { current: initialValue }. Este objeto é armazenado pelo React, então durante a próxima renderização o mesmo objeto será retornado. Note como o setter do state não é utilizado neste exemplo. Ele é desnecessário porque useRef precisa sempre retornar o mesmo objeto!

O React oferece uma versão integrada do useRef porque é algo comum na prática. No entanto, você pode pensar nele como uma variável de state normal sem um setter. Se você está familiarizado com programação orientada a objetos, refs podem te lembrar dos campos de instância — mas em vez de this.something você escreve somethingRef.current.

Quando usar refs

Normalmente, você usará um ref quando o seu componente precisa “sair” do React e se comunicar com APIs externas, frequentemente uma API do navegador que não afetará a aparência do componente. Aqui estão algumas destas situações raras:

Se o seu componente precisa armazenar algum valor, mas isso não afeta a lógica de renderização, escolha refs.

Melhores práticas para refs

Seguir esses princípios tornará seus componentes mais previsíveis:

  • Trate refs como uma saída de emergência. Refs são úteis quando você trabalha com sistemas externos ou APIs de navegador. Se grande parte da lógica da sua aplicação e fluxo de dados dependem de refs, talvez seja necessário repensar suaa abordagem.
  • Não leia nem escreva sobre ref.current durante a renderização. Se alguma informação for necessária durante a renderização, use state. Como o React não sabe quando ref.current muda, até mesmo a leitura durante a renderização torna o comportamento do seu componente difícil de prever. (A única exceção a isso é código como if (!ref.current) ref.current = new Thing(), que define o ref apenas uma vez durante a primeira renderização.)

As limitações do state do React não se aplicam aos refs. Por exemplo, o state age como uma foto instantânea para cada renderização e não atualiza de forma síncrona. Mas quando você altera o valor atual de um ref, ele muda imediatamente:

ref.current = 5;
console.log(ref.current); // 5

Isso é porque o ref em si é um objeto JavaScript normal, e portanto se comporta como um.

Você também não precisa se preocupar em evitar a mutação quando trabalha com um ref. Desde que o objeto que você está mutando não seja usado para renderização, o React não se importa com o que você faz com o ref ou seu conteúdo.

Refs e o DOM

You can point a ref to any value. However, the most common use case for a ref is to access a DOM element. For example, this is handy if you want to focus an input programmatically. When you pass a ref to a ref attribute in JSX, like <div ref={myRef}>, React will put the corresponding DOM element into myRef.current. Once the element is removed from the DOM, React will update myRef.current to be null. You can read more about this in Manipulating the DOM with Refs.

Recap

  • Refs são uma saída de emergência para manter valores que não são usados para renderização. Você não precisará deles com frequência.
  • Um ref é um objeto JavaScript simples com uma única propriedade chamada current, que você pode ler ou definir.
  • Você pode solicitar um ref ao React chamando o Hook useRef.
  • Assim como o state, refs permitem que você retenha informações entre re-renderizações de um componente.
  • Ao contrário do state, definir o valor current do ref não provoca uma re-renderização.
  • Não leia nem escreva sobre ref.current durante a renderização. Isso torna o comportamento do seu componente difícil de prever.

Challenge 1 of 4:
Consertar um input de chat quebrado

Digite uma mensagem e clique em “Send”. Você perceberá que há um atraso de três segundos antes de ver o alerta “Sent!“. Durante esse atraso, você pode ver um botão “Undo”. Clique nele. Este botão “Undo” deve impedir que a mensagem “Sent!” apareça. Ele faz isso chamando clearTimeout para o ID do temporizador salvo durante handleSend. No entanto, mesmo depois de clicar em “Undo”, a mensagem “Sent!” ainda aparece. Descubra por que isso não funciona e corrija-o.

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  let timeoutID = null;

  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      alert('Sent!');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Sending...' : 'Send'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Undo
        </button>
      }
    </>
  );
}