React: useRef e manipulação do DOM

Última atualização: 2026-08-26

useRef É como um cofre — você pode guardar qualquer coisa lá dentro, e ela ainda estará lá quando você a retirar. Ao contrário do useState, alterar o valor do ref não fará com que o componente seja renderizado novamente.


1. O que você vai aprender



2. A história de uma barra de pesquisa

Os dois principais usos do useRef

100%
flowchart LR
    subgraph "Uses1: DOMQuote"
        A[useRef] --> B["ref={inputRef}"]
        B --> C["inputRef.current.focus()"]
    end
    
    subgraph "Uses2: Variable-Value Storage"
        D[useRef] --> E[".current = value]
        E --> F[Does not trigger a repaint]
    end
    
    G[forwardRef] --> H[Passed from the parent componentref]
    H --> I[Child Component ExposureDOM]
    
    style A fill:#e3f2fd,stroke:#1565c0
    style D fill:#e8f5e9,stroke:#2e7d32
    style G fill:#fff3e0,stroke:#e65100

(1) Problema: Para selecionar um campo de entrada, é necessário manipular o DOM

Bob criou uma página de busca e queria que a página focasse automaticamente no campo de busca ao carregar:

JSX
function SearchPage() {
  // ❌ Question:There is no direct way to focus on the input field.
  return (
    <div>
      <input type="text" placeholder="Search..." />
      {/* How can I make this input field automatically receive focus?? */}
      {/* Cannot be used document.getElementById,React Direct manipulation is not recommended. DOM */}
    </div>
  )
}
▶ Experimente

Bob quer usar useState para implementar:

JSX
// ❌ Error:useState Updates trigger a re-render,But I don't know how to use it DOM
function SearchPage() {
  const [input, setInput] = React.useState(null)

  React.useEffect(() => {
    // Hope input is  DOM Element...
    // But how do you DOM Element Storage state in ?
  }, [])

  return <input ref={el => setInput(el)} />  // Function ref,The grammar is strange
}
▶ Experimente

(2) Uma solução utilizando useRef

JSX
import { useRef, useEffect } from 'react'

function SearchPage() {
  const inputRef = useRef(null)  // Create ref

  useEffect(() => {
    // ✅ Automatically focus on the component after it is mounted
    inputRef.current.focus()
  }, [])

  return <input ref={inputRef} type="text" placeholder="Autofocus search box..." />
}
▶ Experimente

Benefícios: useRef retorna um objeto mutável { current: null }, e .current é um elemento DOM real. Não são necessários seletores nem getElementById — é naturalmente compatível com o React.



3. Duas principais formas de uso do useRef

Finalidade ref.current aponta para Aciona uma atualização de renderização Cenários típicos
Referência do DOM Elemento do DOM Foco, rolagem, medição de dimensões
Armazenamento de variáveis Qualquer valor em JavaScript ID do temporizador, valor anterior, estado fora de renderização

(1) Caso de uso 1: Referências ao DOM

Os usos mais comuns — recuperar elementos do DOM, manipular o foco, o tamanho, a posição de rolagem e assim por diante.

Operação Código Descrição
Foco inputRef.current.focus() Campo de entrada do foco automático
Selecionar texto inputRef.current.select() Selecionar tudo no campo de entrada
Ir até divRef.current.scrollIntoView() Ir até um elemento específico
Ler dimensões divRef.current.offsetHeight Obter a altura do elemento
Reproduzir vídeo videoRef.current.play() Controlar a reprodução do vídeo

▶ Exemplo: 5 operações comuns no DOM

JSX
// ============================================
// Example:useRef 's  5 Common Types DOM Operation
// ============================================

function DomOperations() {
  const inputRef = useRef(null)
  const videoRef = useRef(null)
  const listRef = useRef(null)
  const boxRef = useRef(null)
  const [boxHeight, setBoxHeight] = React.useState(0)

  // 1. Autofocus
  useEffect(() => { inputRef.current.focus() }, [])

  function handleSelect() { inputRef.current.select() }       // 2. Select All
  function handlePlay() { videoRef.current.play() }           // 3. Play
  function handlePause() { videoRef.current.pause() }         // 3. Pause
  function handleScroll() { listRef.current.scrollIntoView({ behavior: 'smooth' }) }  // 4. Scroll
  function handleMeasure() {                                  // 5. Measure Dimensions
    setBoxHeight(boxRef.current.offsetHeight)
  }

  return (
    <div>
      <h3>DOM Example of Operation</h3>
      
      <input ref={inputRef} placeholder="Autofocus input field" />
      <button onClick={handleSelect}>Select All Text</button>

      <div ref={listRef} style={{ height: '100px', overflow: 'auto', border: '1px solid #ddd', margin: '10px 0' }}>
        {Array.from({ length: 20 }, (_, i) => <p key={i}>Row {i + 1}</p>)}
      </div>
      <button onClick={handleScroll}>Scroll to the bottom</button>

      <div ref={boxRef} style={{ padding: '20px', backgroundColor: '#f0f0f0', margin: '10px 0' }}>
        <p>The height of this element is:{boxHeight}px</p>
        <button onClick={handleMeasure}>Measure the height</button>
      </div>
    </div>
  )
}
▶ Experimente

(2) Caso de uso 2: Armazenamento de valores de variáveis (sem acionar uma atualização da tela)

Esse é o truque secreto do useRef — ele armazena qualquer valor, e o componente não será renderizado novamente quando esse valor mudar.

JSX
function Stopwatch() {
  const [time, setTime] = React.useState(0)
  const timerRef = useRef(null)  // Storage Timer ID,Do not participate in the rendering

  function start() {
    if (timerRef.current) return  // Prevent Repeated Starts
    
    timerRef.current = setInterval(() => {
      setTime(t => t + 1)
    }, 1000)
  }

  function stop() {
    clearInterval(timerRef.current)
    timerRef.current = null  // Reset
  }

  return (
    <div>
      <p>Timer: {time}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </div>
  )
}
// timerRef Storage Timer ID
// Change timerRef.current Will not trigger a re-render
// The timer is not automatically cleared when the component is unloaded.(I remember back in useEffect Clean up)
▶ Experimente

4. useRef x useState

Dimensão useRef useState
Valor de retorno { current: initial } [value, setter]
Método de modificação ref.current = newValue setState(newValue)
Renderização após modificação ❌ Não aciona ✅ Aciona uma nova renderização
Quando é lido Lê o valor mais recente imediatamente Não o lê até a próxima renderização
Casos de uso Operações no DOM, IDs de temporizadores, valores anteriores Dados a serem exibidos na interface do usuário
JSX
function RefVsState() {
  const renderCount = useRef(1)     // A counter that won't be triggered
  const [count, setCount] = useState(0) // Counters that will be triggered

  useEffect(() => {
    renderCount.current += 1  // Change it however you like,Does not trigger a repaint
  })

  return (
    <div>
      <p>useState:{count}(Click to trigger rendering)</p>
      <p>useRef:{renderCount.current}(View only in the console)</p>
      <button onClick={() => setCount(c => c + 1)}>useState +1</button>
      <button onClick={() => renderCount.current += 1}>useRef +1(No change)</button>
    </div>
  )
}
▶ Experimente

▶ Exemplo: Usando useRef para salvar o valor anterior

JSX
// ============================================
// Example:Go back to props/state value
// ============================================

function usePrevious(value) {
  const ref = useRef()

  useEffect(() => {
    ref.current = value  // Update after each render
  })

  return ref.current  // Go back to the previous value
}

function Counter() {
  const [count, setCount] = useState(0)
  const prevCount = usePrevious(count)  // The previous value

  return (
    <div>
      <p>Currently:{count}</p>
      <p>Last time:{prevCount !== undefined ? prevCount : '(For the first time)'}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  )
}
// Process:
// 1. Initial Rendering:count=0, prevCount=undefined
// 2. Click +1 → count=1
// 3. useEffect Update prev=0 → Back 0
// 4. Click again +1 → count=2, prev=1
▶ Experimente

5. forwardRef: Componentes pais controlam o DOM dos componentes filhos

Por padrão, os componentes de função não expõem suas próprias referências ao DOM. O uso de forwardRef permite que os componentes filhos recebam o ref do componente pai:

JSX
// ---- Child component:use  forwardRef Package ----
const CustomInput = forwardRef(function CustomInput(props, ref) {
  return (
    <div style={{ border: '1px solid #d9d9d9', padding: '4px', borderRadius: '4px' }}>
      <input ref={ref} {...props} style={{ border: 'none', outline: 'none', width: '100%' }} />
    </div>
  )
})

// ---- Parent Component:Use directly ref Controlling subcomponents input ----
function Form() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current.focus()  // ✅ Can focus directly on CustomInput Internal input
  }, [])

  return (
    <div>
      <CustomInput ref={inputRef} placeholder="Input fields controlled by the parent component" />
      <button onClick={() => inputRef.current.focus()}>In Focus</button>
      <button onClick={() => inputRef.current.select()}>Select All</button>
    </div>
  )
}
▶ Experimente

6. useImperativeHandle: Expor um método específico

Se você não quiser expor todo o elemento DOM, use useImperativeHandle para expor apenas métodos específicos:

JSX
// ============================================
// Example:Custom Player——Expose only play/pause,Without revealing the whole video
// ============================================

const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
  const videoRef = useRef(null)

  // Expose only 3 A method for the parent component
  useImperativeHandle(ref, () => ({
    play() {
      videoRef.current.play()
    },
    pause() {
      videoRef.current.pause()
    },
    jumpTo(seconds) {
      videoRef.current.currentTime = seconds
    }
  }))

  return <video ref={videoRef} src={src} controls style={{ width: '100%' }} />
})

// ---- Usage -----
function App() {
  const playerRef = useRef(null)

  return (
    <div>
      <h3>Video Player</h3>
      <VideoPlayer ref={playerRef} src="https://example.com/video.mp4" />
      <div style={{ marginTop: '8px', display: 'flex', gap: '8px' }}>
        <button onClick={() => playerRef.current.play()}>▶ Play</button>
        <button onClick={() => playerRef.current.pause()}>⏸ Pause</button>
        <button onClick={() => playerRef.current.jumpTo(30)}>⏭ Jump to 30s</button>
      </div>
      {/* The parent component cannot be manipulated directly video DOM,You can only call the exposed play/pause/jumpTo */}
    </div>
  )
}
▶ Experimente

7. Exemplo completo: Editor de texto rico

JSX
// ============================================
// Complete Example:Simple Rich Text Editor
// Features:useRef Control contentEditable Region
//       forwardRef + useImperativeHandle Exposure Methods
// ============================================

const RichEditor = forwardRef(function RichEditor({ placeholder }, ref) {
  const editorRef = useRef(null)
  const [isEmpty, setIsEmpty] = React.useState(true)

  // Exposing Methods to the Parent Component
  useImperativeHandle(ref, () => ({
    getContent() {
      return editorRef.current.innerHTML
    },
    setContent(html) {
      editorRef.current.innerHTML = html
      checkEmpty()
    },
    clear() {
      editorRef.current.innerHTML = ''
      setIsEmpty(true)
      editorRef.current.focus()
    },
    focus() {
      editorRef.current.focus()
    }
  }))

  function checkEmpty() {
    const text = editorRef.current.textContent || ''
    setIsEmpty(text.trim().length === 0)
  }

  function handleKeyDown(e) {
    if (e.ctrlKey && e.key === 'b') {
      document.execCommand('bold')
      e.preventDefault()
    }
    if (e.ctrlKey && e.key === 'i') {
      document.execCommand('italic')
      e.preventDefault()
    }
  }

  return (
    <div style={{ border: '1px solid #d9d9d9', borderRadius: '4px', overflow: 'hidden' }}>
      {/* Toolbar */}
      <div style={{ padding: '8px', borderBottom: '1px solid #d9d9d9', backgroundColor: '#fafafa', display: 'flex', gap: '4px' }}>
        <button onMouseDown={e => { e.preventDefault(); document.execCommand('bold') }} style={toolBtnStyle}><b>B</b></button>
        <button onMouseDown={e => { e.preventDefault(); document.execCommand('italic') }} style={toolBtnStyle}><i>I</i></button>
        <button onMouseDown={e => { e.preventDefault(); document.execCommand('underline') }} style={toolBtnStyle}><u>U</u></button>
        <span style={{ color: '#ddd' }}>|</span>
        <button onMouseDown={e => { e.preventDefault(); document.execCommand('insertUnorderedList') }} style={toolBtnStyle}>List</button>
        <button onMouseDown={e => { e.preventDefault(); document.execCommand('formatBlock', false, 'h2') }} style={toolBtnStyle}>H2</button>
      </div>

      {/* Edit Area */}
      <div
        ref={editorRef}
        contentEditable
        onInput={checkEmpty}
        onKeyDown={handleKeyDown}
        style={{
          minHeight: '200px',
          padding: '16px',
          outline: 'none',
          lineHeight: '1.6'
        }}
        data-placeholder={placeholder}
        {...(isEmpty ? { 'data-empty': 'true' } : {})}
      />
    </div>
  )
})

function App() {
  const editorRef = useRef(null)
  const [savedContent, setSavedContent] = React.useState('')

  function handleSave() {
    const content = editorRef.current.getContent()
    setSavedContent(content)
    alert('Saved!')
  }

  function handleClear() {
    editorRef.current.clear()
  }

  function handleLoad() {
    editorRef.current.setContent('<h2>Loaded content</h2><p>This is loaded from an external source. HTML。</p>')
  }

  return (
    <div style={{ maxWidth: '700px', margin: '0 auto' }}>
      <h2>📝 Rich Text Editor</h2>
      
      <RichEditor ref={editorRef} placeholder="Start Writing..." />

      <div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
        <button onClick={handleSave} style={btnStyle('#1890ff')}>💾 Save</button>
        <button onClick={handleLoad} style={btnStyle('#52c41a')}>📂 Loading Example</button>
        <button onClick={handleClear} style={btnStyle('#ff4d4f')}>🗑 Clear</button>
      </div>

      {savedContent && (
        <div style={{ marginTop: '16px', padding: '16px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
          <p style={{ fontWeight: 'bold', margin: '0 0 8px 0' }}>Saved Content:</p>
          <div style={{ fontSize: '13px', color: '#666', wordBreak: 'break-all', fontFamily: 'monospace' }}>
            {savedContent}
          </div>
        </div>
      )}
    </div>
  )
}

const toolBtnStyle = {
  padding: '4px 10px', border: '1px solid transparent',
  borderRadius: '3px', backgroundColor: 'transparent',
  cursor: 'pointer', fontSize: '14px'
}

const btnStyle = (color) => ({
  padding: '8px 16px', backgroundColor: color,
  color: 'white', border: 'none',
  borderRadius: '4px', cursor: 'pointer'
})

Resultado esperado: Um editor de texto rico que ofereça suporte a negrito, itálico, sublinhado, listas, títulos H2 e outras opções de formatação; o conteúdo pode ser salvo, carregado ou apagado externamente por meio de um ref.


▶ Exemplo 3: Como usar useRef para fechar uma janela pop-up ao clicar nela de fora

JSX
function useClickOutside(callback) {
  const ref = useRef(null)

  useEffect(() => {
    function handler(e) {
      if (ref.current && !ref.current.contains(e.target)) {
        callback()
      }
    }
    document.addEventListener('mousedown', handler)
    return () => document.removeEventListener('mousedown', handler)
  }, [callback])

  return ref
}

function Dropdown() {
  const [open, setOpen] = useState(false)
  const dropdownRef = useClickOutside(() => setOpen(false))

  return (
    <div ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
      <button onClick={() => setOpen(!open)} style={{ padding: '8px 16px', cursor: 'pointer' }}>
        Menu ▾
      </button>
      {open && (
        <div style={{ position: 'absolute', top: '100%', left: 0, background: 'white', border: '1px solid #ddd', borderRadius: 4, minWidth: 120, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
          {['Profile', 'Settings', 'Logout'].map(item => (
            <div key={item} onClick={() => { setOpen(false) }}
              style={{ padding: '8px 16px', cursor: 'pointer' }}>
              {item}
            </div>
          ))}
        </div>
      )}
    </div>
  )
}
▶ Experimente

▶ Exemplo 4: Usando useRef para armazenar um temporizador na implementação de um botão com limitação de frequência

JSX
function ThrottledButton() {
  const lastClickRef = useRef(0)
  const [clicks, setClicks] = useState(0)
  const [feedback, setFeedback] = useState('')

  function handleClick() {
    const now = Date.now()
    if (now - lastClickRef.current < 1000) {
      setFeedback('Too fast! Wait 1 second.')
      return
    }
    lastClickRef.current = now
    setClicks(c => c + 1)
    setFeedback('Clicked!')
    setTimeout(() => setFeedback(''), 500)
  }

  return (
    <div style={{ textAlign: 'center', padding: 20 }}>
      <button onClick={handleClick}
        style={{ padding: '10px 24px', fontSize: 16, cursor: 'pointer' }}>
        Click Me
      </button>
      <p>Clicks: {clicks}</p>
      {feedback && <p style={{ color: feedback.includes('Too') ? '#ff4d4f' : '#52c41a' }}>{feedback}</p>}
    </div>
  )
}
▶ Experimente

▶ Exemplo 5: Usando o forwardRef para envolver um componente Input que pode receber foco

JSX
const FancyInput = React.forwardRef(function FancyInput({ label, error, ...props }, ref) {
  const internalRef = useRef(null)

  useImperativeHandle(ref, () => ({
    focus: () => internalRef.current?.focus(),
    selectAll: () => {
      const el = internalRef.current
      if (el) { el.focus(); el.select() }
    },
    scrollIntoView: () => internalRef.current?.scrollIntoView({ behavior: 'smooth' }),
  }))

  return (
    <div style={{ marginBottom: 12 }}>
      <label style={{ display: 'block', marginBottom: 4, fontSize: 14, fontWeight: 500 }}>{label}</label>
      <input
        ref={internalRef}
        style={{ width: '100%', padding: 8, borderRadius: 4, border: `1px solid ${error ? '#ff4d4f' : '#d9d9d9'}`, outline: 'none' }}
        {...props}
      />
      {error && <p style={{ color: '#ff4d4f', fontSize: 12, margin: '4px 0 0' }}>{error}</p>}
    </div>
  )
})

function FormWithFancyInput() {
  const emailRef = useRef(null)

  function handleSubmit(e) {
    e.preventDefault()
    emailRef.current?.focus()
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: 400, margin: '0 auto' }}>
      <h3>Sign Up</h3>
      <FancyInput ref={emailRef} label="Email" type="email" placeholder="you@example.com" />
      <FancyInput label="Password" type="password" placeholder="At least 6 characters" />
      <button type="submit" style={{ padding: '8px 24px', cursor: 'pointer' }}>Submit</button>
    </form>
  )
}
▶ Experimente

❓ Perguntas Frequentes

P: Qual é a diferença entre useRef e document.querySelector? R: ① useRef não depende de seletores DOM e não se tornará inválido se os nomes de classe ou IDs forem alterados; ② useRef é declarativo (vinculado por meio do atributo ref), enquanto querySelector é imperativo (pesquisa manual); ③ No React, o DOM é gerenciado pelo DOM virtual; o uso de querySelector contorna os mecanismos do React e pode levar a inconsistências. Sempre use useRef primeiro.

P: ref.current Quando ele é nulo? R: ① Quando o componente ainda não foi montado (antes de useEffect); ② Quando a condição para um componente renderizado condicionalmente não é atendida (em {show && <div ref={ref} />}, ref.current é nulo quando show é falso); ③ Quando o componente foi desmontado. Sempre verifique se há nulo antes de operar em ref: ref.current?.focus().

P: Quando devo usar ref para armazenar valores e quando devo usar useState? R: Se o valor precisar ser exibido na interface do usuário → use useState; se não precisar ser exibido na interface do usuário → use useRef. Por exemplo: IDs de temporizadores (invisíveis aos usuários), valores anteriores (usados apenas para comparação) e rastreamento da posição de rolagem (não exibida na página) → todos usam useRef. Alterações em useRef não acionam uma renderização — essa é a principal diferença.

P: É necessário usar o forwardRef em todos os componentes? R: Não. Use-o apenas nos seguintes cenários: ① Quando um componente pai precisa manipular diretamente o DOM de um componente filho (por exemplo, para colocar o foco em um campo de entrada ou controlar a reprodução de mídia); ② Ao encapsular componentes de formulário reutilizáveis (para que o componente pai possa controlar o foco ou a seleção). Se você estiver apenas passando dados, basta usar props.

P: O useRef pode substituir o useState? R: Não. Alterações no useRef não acionam uma nova renderização do componente, ao passo que o setState sim. Se o seu valor precisar ser exibido na interface do usuário ou afetar o resultado da renderização, você deve usar o useState. Se você estiver simplesmente armazenando “dados que não precisam ser mostrados ao usuário” (como um ID de intervalo, o valor da renderização anterior ou uma instância de WebSocket), use useRef. Pense em useRef como um “contêiner de variáveis que não aciona uma nova renderização”.


📖 Resumo


📝 Exercícios

  1. Exercício básico (Dificuldade ⭐): Crie um componente AutoFocusInput que receba foco automaticamente ao ser montado e forneça dois métodos — focus() e clear() — para que o componente pai os chame.
  2. Exercício avançado (Dificuldade ⭐⭐): Crie um componente ClickCounter, use useRef para rastrear o número de cliques (sem exibi-lo na interface do usuário) e use useEffect para registrar “Clicado X vezes” no console a cada renderização.
  3. Desafio (Dificuldade: ⭐⭐⭐): Crie um componente InfiniteScroll que utilize useRef para detectar quando o elemento sentinela (um elemento específico na parte inferior da página) entrar na área de visualização, acionando o carregamento de mais dados. Utilize a API IntersectionObserver.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%