React – Refs

July 27, 20265 min readUpdated 8/18/2026

A ref is a value a component remembers across renders that does not trigger a re-render when it changes. It has two uses: reaching a real DOM node, and holding a mutable value that the screen does not depend on.

ref versus state

useStateuseRef
Changing it re-rendersyesno
Survives re-rendersyesyes
How you change itsetX(next)ref.current = next
Mutableno — replace ityes — assign to .current
Safe to read during renderyesno

The decision is one question: does the screen need to change when this value changes? Yes means state. No means a ref.

Reaching a DOM node

React builds the DOM for you, and 95% of the time you never touch it. The exceptions are the things the DOM does that React has no declarative equivalent for: focus, scroll position, measuring, media playback, and canvas.

Three steps — create the ref, attach it, use it after render:

const confirmButtonRef = useRef<HTMLButtonElement>(null);

<Button ref={confirmButtonRef} variant="primary" onClick={handleAdd}>
  Add to cart
</Button>

The pizza builder uses it to move focus onto the confirm button when the modal opens — which matters, because a keyboard or screen-reader user is otherwise left at the top of the document with no idea a dialog appeared:

<Modal
  show={product !== null}
  onHide={onHide}
  size="lg"
  centered
  scrollable
  // Bootstrap's Modal already traps focus and restores it on close; this just picks the
  // element that should receive focus first.
  onEntered={() => confirmButtonRef.current?.focus()}
  aria-labelledby="builder-title"
>

?. is not optional politeness. ref.current is null until React has attached the node, and null again after it unmounts. Always guard.

Typing it

const buttonRef = useRef<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);

The type is the element you are attaching to. Passing null as the initial value is what tells TypeScript this is a DOM ref rather than a mutable value ref — the two have subtly different types.

When you can read it

Not during render. The node does not exist yet on the first render, and touching the DOM during render breaks purity. Read it in an event handler or an effect:

// Wrong — current is null on the first render, and this is a side effect during render.
function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);
  inputRef.current?.focus();
  return <input ref={inputRef} />;
}

// Right.
function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  return <input ref={inputRef} />;
}

What not to do with it

A ref is an escape hatch, not a back door. Use it to call methods on a node — focus, scrollIntoView, play, measure. Do not use it to change what React is rendering:

// Do not. React does not know about this and will overwrite it on the next render.
divRef.current.style.display = 'none';
divRef.current.textContent = 'Sold out';

// Do. React owns what is on the screen.
{soldOut ? <span>Sold out</span> : <Price value={price} />}

Holding a value between renders

The second use has nothing to do with the DOM. Sometimes a component needs to remember something that the display does not depend on — and putting it in state would cause a pointless re-render, or worse, a loop.

The cart provider stores the server-side cart id in a ref:

/*
 * A ref, not state: the persist effect needs the CURRENT cart id without re-running every time
 * the id changes (which would cause an extra PUT).
 */
const cartIdRef = useRef<string | null>(cartIdStore.get());

Trace why. The persist effect reads the id, and creates a cart on the server if there is not one yet. As state, the id would have to be in the effect's dependency array — so setting it would re-run the effect, which would issue a second PUT. As a ref, the effect reads the latest value and nothing re-runs:

let cartId = cartIdRef.current;

if (!cartId) {
  // Do not create a cart row just because someone loaded the home page.
  if (state.items.length === 0) return;
  const created = await api.post<ServerCart>('/api/carts');
  cartId = created.id;
  cartIdRef.current = cartId;
  cartIdStore.set(cartId);
}

await api.put<ServerCart>(`/api/carts/${cartId}`, toWriteRequest(state));

The id genuinely is not display state — nothing on the screen shows it. That is what makes a ref the right call rather than a trick.

The other classic is a timer id, which you need in order to cancel it and which no part of the UI displays:

const timerRef = useRef<number | null>(null);

function startCountdown() {
  timerRef.current = window.setTimeout(() => { /* … */ }, 3000);
}

function cancel() {
  if (timerRef.current !== null) window.clearTimeout(timerRef.current);
}

Reading or writing a ref during render is a bug

// WRONG on both lines. React may render twice, throw the result away, or pause — none of which
// this code survives, and StrictMode will show you exactly that.
function ProductCard({ product }: Props) {
  renderCountRef.current++;
  return <div>{renderCountRef.current}</div>;
}

Refs are read and written in event handlers and effects. If you need a value during render, it is either state, a prop, or something you can calculate.

forwardRef is no longer needed

Every tutorial written before React 19 has a section on forwardRef, because passing a ref to your own component used to require it — ref was not a real prop.

// React 18 and earlier.
const FancyInput = forwardRef<HTMLInputElement, Props>(function FancyInput(props, ref) {
  return <input ref={ref} {...props} />;
});

// React 19. `ref` is an ordinary prop.
function FancyInput({ ref, ...props }: Props & { ref?: Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />;
}

forwardRef still works and is not urgent to remove. New code does not need it.

Callback refs

A ref prop can be a function instead of a ref object. React calls it with the node when it mounts and with null when it unmounts — useful when you need to react to the node appearing, or when you are collecting refs for a dynamic list:

<div ref={(node) => { if (node) observer.observe(node); }} />

In React 19 a callback ref may also return a cleanup function, which is tidier than checking for null.

Next

Error Boundaries — the last escape hatch, and the one place a class component is still mandatory.