Skip to content

Refs and the DOM (two different "ref" words) ​

In Vue, ref() is reactive state and sometimes a template ref. In React, useRef is a mutable box that survives renders - and also how you grab DOM nodes.

useRef as a mutable box ​

tsx
import { useRef } from 'react'

function Timer() {
  const ticks = useRef(0)

  function onClick() {
    ticks.current += 1
    console.log(ticks.current)
  }

  return <button type="button" onClick={onClick}>Tick</button>
}

Comments for the code above:

  • Changing .current does not re-render (unlike Vue ref)
  • Useful for interval IDs, previous values, anything imperative that isn't UI state
  • If the UI must update, use useState

DOM refs ​

tsx
function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

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

  return <input ref={inputRef} />
}

As you can see - ref={inputRef} is the template ref equivalent of ref="inputRef" / const el = ref(null) in Vue.

forwardRef (when a parent needs a child's DOM node) ​

Custom components don't forward refs by default. If you wrap an <input> and want the parent to focus it:

tsx
import { forwardRef } from 'react'

const FancyInput = forwardRef<HTMLInputElement, { label: string }>(
  function FancyInput({ label }, ref) {
    return (
      <label>
        {label}
        <input ref={ref} />
      </label>
    )
  },
)

Comments for the code above:

  • Parent does <FancyInput ref={inputRef} label="Email" />
  • React 19 makes ref-as-prop easier in some cases - check your version; forwardRef still works everywhere

What I'd avoid ​

  • Storing props/state copies in refs "to avoid re-renders" as a default strategy - usually wrong
  • Touching the DOM for things React already does (toggling classes that could be state)

Refs are escape hatches. Use them when you must talk to the browser imperatively - then get back to declarative land.

Next: Context - provide/inject with more ceremony.

Enjoy!