How useRef persists mutable values across renders and provides direct DOM access.
useRef returns a mutable object { current: value } that persists for the full lifetime of the component. Unlike state, mutating ref.current does NOT trigger a re-render. React also uses refs as an escape hatch for direct DOM access β after the commit phase, React sets ref.current to the actual DOM node, giving you imperative access to the browser API.
On first render, useRef(initialValue) creates the object '{' current: initialValue '}' and stores it on the fiber's hooks list. On every subsequent render, React returns the same object β so ref.current always points to the same box.
React does not track ref.current mutations. When you write ref.current = 5, React has no idea it changed and will not schedule a re-render. This makes refs ideal for values you want to persist but don't need to display.
When you pass a ref to a JSX element (<input ref='{'myRef'}' />), React does not set ref.current during the render phase. It waits until the commit phase β after the DOM node exists β then sets ref.current = domNode.
When the component unmounts, React sets ref.current = null before running cleanup effects. This prevents your effects from accessing a detached DOM node.
By default, refs cannot be attached to function components. Use forwardRef to accept a ref prop and forward it to a DOM element or expose imperative methods via useImperativeHandle.
The ref object. React gives you the same object every render. Mutate current freely β no re-render will happen.
Pass a ref to a JSX element to get the underlying DOM node in ref.current after commit. Useful for focus, scroll, and measurement.
Wraps a function component to accept a ref prop and forward it to an inner element or expose via useImperativeHandle.
Customizes what the parent sees when it accesses a child's ref. Instead of the DOM node, you can expose specific methods.
Use state for values that affect the UI (triggers re-render). Use refs for values that don't affect rendering: timers, DOM nodes, previous values, flags.
1// 1. DOM access β focus an input programmatically2const inputRef = useRef(null);3<input ref={inputRef} />4// After mount: inputRef.current is the <input> DOM node5inputRef.current.focus(); // imperative DOM call67// 2. Persist a value without re-rendering8const timerRef = useRef(null);9timerRef.current = setInterval(tick, 1000);10// Later:11clearInterval(timerRef.current);1213// 3. Read latest state in a stale closure14const countRef = useRef(count);15countRef.current = count; // sync on every render16useEffect(() => {17 const interval = setInterval(() => {18 console.log(countRef.current); // always latest value19 }, 1000);20 return () => clearInterval(interval);21}, []); // empty deps β no stale closure problem
Refs are React's official escape hatch for imperative code. They let you bridge React's declarative world with browser APIs that require imperative calls (focus, scroll, animations, third-party DOM libraries). They also solve the stale closure problem in effects β by keeping a ref in sync with the latest state, you can safely access current values inside long-running async operations.
A multi-step checkout form should automatically focus the first empty field when the user navigates to a new step. Using state to track focus causes infinite re-renders.
Attempting to manage focus with useState creates a loop: setState triggers re-render, re-render re-focuses, which fires onFocus, which might set state again. Focus is an imperative DOM operation β it doesn't fit React's declarative model.
Use a ref to directly call `.focus()` on the input DOM node: `useEffect(() => { inputRef.current?.focus() }, [currentStep])`. The ref gives you direct DOM access without triggering re-renders. Focus management is inherently imperative β refs are React's bridge to imperative DOM APIs.
Takeaway: Use refs for imperative DOM operations that don't produce visual output (focus, scroll, measure, select text, play/pause media). These operations are side effects that bypass React's virtual DOM β refs give you a controlled escape hatch.
You need to compare the current value with the previous value to show a 'price changed' animation. Storing the previous value in state causes an extra re-render on every change.
Using `useState(previousPrice)` to track the old price means: price changes β re-render with new price β useEffect sets previousPrice β ANOTHER re-render. Two renders for one price change, and the animation triggers late.
Use a ref: `const prevPriceRef = useRef(price)`. In useEffect, compare `price !== prevPriceRef.current`, trigger the animation, then update: `prevPriceRef.current = price`. Refs update without re-rendering β you get the comparison for free, no extra render cycle.
Takeaway: Refs are the correct tool for 'instance variables' β values that need to persist across renders but shouldn't trigger re-renders when updated. Common patterns: previous values, timer IDs, DOM nodes, external library instances, and accumulator values.