How to type useRef.
DOM refs vs mutable refs.
useRef has two completely different uses — pointing at a DOM element, and storing a mutable value that survives renders. Both use the same hook, but the types are different. Get the generic wrong and TypeScript fights you. Get it right and refs are some of the cleanest code in React.
Short answer: Two patterns. DOM ref: useRef — element type, null initial value, access via ref.current?.focus();. Mutable value: useRef — value type, actual initial value, access via ref.current. In both cases, the generic is the type of .current. Never type it as any or RefObject — you'll lose every method on the element.
✅ Solution 1 — DOM refs
The pattern for accessing a real element.
A DOM ref points to a real element in the browser. The generic is the element type (HTMLInputElement, HTMLDivElement, HTMLButtonElement), and the initial value is null — because the element doesn't exist until React attaches it after render.
import { useRef } from 'react'; function FocusDemo() { // ─── The canonical DOM ref ─────────────────── const inputRef = useRef(null); // inputRef.current: HTMLInputElement | null function focus() { // Narrow with ?. — ref might not be attached yet inputRef.current?.focus(); // inputRef.current.select(); // ❌ possibly null } function clear() { if (inputRef.current) { inputRef.current.value = ''; // ✅ narrowed by if inputRef.current.focus(); } } return ( "Type here..." />); } // ─── Other element types ───────────────────── function ManyRefs() { const divRef = useRef(null); const buttonRef = useRef (null); const videoRef = useRef (null); const canvasRef = useRef (null); const formRef = useRef (null); return ( ); } // ─── Common DOM element types ───────────────── // // HTMLInputElement — // HTMLTextAreaElement —
Generic = element type
useRef — the generic is the actual element. .current has every property and method of that element.
null is required
The element doesn't exist until React attaches it. useRef — .current is undefined, not null. Always pass null.
Narrow before access
ref.current is T | null. Use ?. for optional chains, or if (ref.current) to narrow for multiple operations.
Attach via ref={...}
Same prop on any element. React fills .current with the actual DOM node after mount, sets it back to null on unmount.
Hover over ref.current in VS Code to see the exact type. It's the fastest way to learn what a ref contains. useRef → HTMLInputElement | null. useRef(0) → number. useRef → Set. If the hover doesn't match what you expect, the generic or the initial value is wrong. This is the debugging technique that removes 90% of ref confusion.
✅ Solution 2 — Mutable value refs
When you need a value that survives renders without triggering them.
The other use of useRef: storing a value across renders that doesn't cause re-renders when it changes. Unlike state, mutating ref.current is silent — no render, no update. Perfect for timers, previous values, and any "latest" reference you need inside callbacks.
import { useRef, useState } from 'react'; // ─── Counter with a mutable ref ────────────── function RefCounter() { const renders = useRef(0); // renders.current: number (always defined, not null) // Increment on every render — no re-render caused by the ref renders.current += 1; returnThis component has rendered {renders.current} times
; } // ─── Timer ref ─────────────────────────────── function Stopwatch() { const [elapsed, setElapsed] = useState(0); const intervalRef = useReftypeof setInterval> | null>(null); // ReturnType — works in both browser and Node function start() { intervalRef.current = setInterval(() => { setElapsed(e => e + 1); }, 1000); } function stop() { if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; } } return (); } // ─── "Previous value" ref ──────────────────── function PreviousValue({ value }: { value: number }) { const prevRef = useRef(value); // Compare to last render's value const prev = prevRef.current; const trend = value > prev ? 'up' : value < prev ? 'down' : 'same'; // Update for next render — silent, no re-render prevRef.current = value; return{elapsed}s
{value} ({trend} from {prev})
; } // ─── Latest callback ref (avoid stale closures) function LatestCallback({ onClick }: { onClick: () => void }) { const callbackRef = useRef(onClick); // Always keep the latest callback callbackRef.current = onClick; // Use in an event — never stale const handleClick = () => callbackRef.current(); return ; } // ─── Map cache ─────────────────────────────── function CachedList({ items }: { items: string[] }) { const cacheRef = useRef<Map<string, number>>(new Map()); function getLength(item: string): number { const cached = cacheRef.current.get(item); if (cached !== undefined) return cached; const len = item.length; cacheRef.current.set(item, len); return len; } return{items.map(i =>
; } console.log(RefCounter, Stopwatch, PreviousValue, LatestCallback, CachedList);- {i}: {getLength(i)}
)}
import { useRef, useState } from 'react'; function RefCounter() { const renders = useRef(0); renders.current += 1; return React.createElement('p', null, 'This component has rendered ', renders.current, ' times'); } function Stopwatch() { const [elapsed, setElapsed] = useState(0); const intervalRef = useRef(null); function start() { intervalRef.current = setInterval(() => { setElapsed(e => e + 1); }, 1000); } function stop() { if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; } } return React.createElement('div', null, React.createElement('p', null, elapsed, 's'), React.createElement('button', { onClick: start }, 'Start'), React.createElement('button', { onClick: stop }, 'Stop') ); } function PreviousValue({ value }) { const prevRef = useRef(value); const prev = prevRef.current; const trend = value > prev ? 'up' : value < prev ? 'down' : 'same'; prevRef.current = value; return React.createElement('p', null, value, ' (', trend, ' from ', prev, ')'); } function LatestCallback({ onClick }) { const callbackRef = useRef(onClick); callbackRef.current = onClick; const handleClick = () => callbackRef.current(); return React.createElement('button', { onClick: handleClick }, 'Click'); } function CachedList({ items }) { const cacheRef = useRef(new Map()); function getLength(item) { const cached = cacheRef.current.get(item); if (cached !== undefined) return cached; const len = item.length; cacheRef.current.set(item, len); return len; } return React.createElement('ul', null, items.map(i => React.createElement('li', { key: i }, i, ': ', getLength(i)))); } console.log(RefCounter, Stopwatch, PreviousValue, LatestCallback, CachedList);
Generic = value type
useRef(0) → number. useRef → Set. .current is always that type.
Mutations are silent
ref.current = x doesn't trigger a render. That's the point — refs are for values that don't need to be displayed.
Use for timers
The setInterval ID must survive renders to cancel it. State would trigger re-renders on every assignment. Refs are the correct tool.
Previous values
Comparing current to previous is a classic use case. Store the previous in a ref — silent, no extra renders.
If .current changes should be visible in the UI, use useState instead. Refs are for values that don't affect rendering — timer IDs, DOM nodes, memoized callbacks, "previous value" comparisons. If you find yourself wanting the component to re-render when ref.current changes, you've picked the wrong hook. The distinction: state drives the UI; refs drive behavior.
✅ Solution 3 — Custom component refs (forwardRef)
When the ref is forwarded to an inner element.
If your component wraps an input (or any element), the parent needs a ref to that inner element. Use forwardRef with two type parameters: the element type and the props type. The parent types its ref with the element — not the component.
import { useRef, forwardRef } from 'react'; // ─── The forwardRef component ──────────────── interface FancyInputProps { label: string; placeholder?: string; } const FancyInput = forwardRef( ({ label, placeholder }, ref) => { // ref is ForwardedRef return ( ); } ); FancyInput.displayName = 'FancyInput'; // ─── Parent uses element type ──────────────── function Parent() { // Generic is HTMLInputElement — the INNER element, not FancyInput const inputRef = useRef (null); function focus() { inputRef.current?.focus(); // ✅ inputRef.current?.select(); // ✅ // inputRef.current.value = ''; // ✅ full HTMLInputElement API } return ( ); } // ─── React 19: ref as a prop ───────────────── // React 19 lets you skip forwardRef entirely. interface NewInputProps { label: string; placeholder?: string; ref?: React.Ref"Name" placeholder="Enter..." /> ; } function NewInput({ label, placeholder, ref }: NewInputProps) { return ( ); } // Parent usage is identical: // const ref = useRef (null); // // ─── Callback refs ─────────────────────────── function CallbackRef() { const heightRef = useRef(0); // Callback runs when element mounts/unmounts function measure(el: HTMLDivElement | null) { if (el) { heightRef.current = el.getBoundingClientRect().height; } } returnMeasured on mount; } console.log(FancyInput, Parent, NewInput, CallbackRef);
import { useRef, forwardRef } from 'react'; const FancyInput = forwardRef(({ label, placeholder }, ref) => { return React.createElement('label', null, label, React.createElement('input', { ref, placeholder }) ); }); FancyInput.displayName = 'FancyInput'; function Parent() { const inputRef = useRef(null); function focus() { inputRef.current?.focus(); inputRef.current?.select(); } return React.createElement('div', null, React.createElement(FancyInput, { ref: inputRef, label: 'Name', placeholder: 'Enter...' }), React.createElement('button', { onClick: focus }, 'Focus') ); } function NewInput({ label, placeholder, ref }) { return React.createElement('label', null, label, React.createElement('input', { ref, placeholder }) ); } function CallbackRef() { const heightRef = useRef(0); function measure(el) { if (el) { heightRef.current = el.getBoundingClientRect().height; } } return React.createElement('div', { ref: measure }, 'Measured on mount'); } console.log(FancyInput, Parent, NewInput, CallbackRef);
forwardRef
Two generics: element type T and props type P. The ref is passed as the second argument to the function.
Parent uses element type
Parent's useRef — the element the ref ultimately points to, not the component.
React 19 ref-as-prop
Skip forwardRef: add ref?: React.Ref to props, use it directly. Simpler, same typing.
Callback refs
ref={(el) => ...} — the callback runs with the element on mount, null on unmount. For measuring, focus, third-party integration.
⚠️ Common mistakes
The ref typing mistakes that cost real time.
useRef() Missing the null. Fix: useRef. Without it, .current is undefined, not null.
anyuseRef — loses every method. Fix: use the actual element type.
?. on DOM refsref.current.focus() — possibly null on first render. Fix: ref.current?.focus().
ref.current = x doesn't re-render. Fix: use useState if the UI needs to update.
— won't work without forwardRef or React 19 ref-as-prop. Fix: use one of those patterns.
ReturnType incorrectly for timersReturnType differs in browser vs Node. Fix: ReturnType.
ref.current during renderRefs are attached after render. Fix: read in effects or events, or use useLayoutEffect for measurement.
displayName on forwardRefDevTools shows "ForwardRef" instead of the component name. Fix: set Component.displayName = 'Component'.
❓ Frequently asked
How do I type useRef in React + TypeScript?
Two patterns. (1) DOM refs: useRef
Why do I need to pass null to useRef?
For DOM refs: the element doesn't exist during the first render — React attaches it after the DOM is created. So the initial value must be null, and ref.current is typed as HTMLInputElement | null. You must narrow before using it.
What is the difference between useRef(null) and useRef(null)?
The first is the DOM ref pattern: ref.current type HTMLInputElement | null, and ref is readonly (can't reassign). The second gives a MutableRefObject where .current is writable. For DOM refs, use the first — you never reassign ref.current manually.
How do I type a ref that points to a custom component?
A custom component's ref forwards to a DOM element underneath. Use forwardRef