Refs cheatsheet.
DOM ref
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus();
}, []);
<input ref={ref} />
Always init with null; access current with optional chaining.
Mutable value
const idRef = useRef(0);
function nextId() {
return ++idRef.current;
}
Survives renders without triggering re-render. Good for IDs, timers, previous values.
Storing previous value
function usePrev<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}
const prev = usePrev(count);
Timer / interval refs
const timerRef = useRef<NodeJS.Timeout | null>(null);
function start() {
timerRef.current = setInterval(tick, 1000);
}
function stop() {
if (timerRef.current) clearInterval(timerRef.current);
}
Callback ref
function MeasuredDiv() {
const [size, setSize] = useState({ w: 0, h: 0 });
const ref = useCallback((node: HTMLDivElement | null) => {
if (!node) return;
setSize({ w: node.offsetWidth, h: node.offsetHeight });
}, []);
return <div ref={ref}>w={size.w}</div>;
}
Called when node mounts / unmounts. Useful when ref-dependent state matters.
forwardRef (pre-React 19)
type Props = { label: string };
const Input = forwardRef<HTMLInputElement, Props>(({ label }, ref) => (
<label>
{label}
<input ref={ref} />
</label>
));
// Use:
const ref = useRef<HTMLInputElement>(null);
<Input ref={ref} label="Name" />
React 19: ref as prop
function Input({ label, ref }: { label: string; ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} />;
}
No forwardRef needed in 19+.
useImperativeHandle
Expose a custom imperative API:
type Handle = { focus: () => void; clear: () => void };
const Input = forwardRef<Handle, { initial: string }>((props, ref) => {
const inner = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inner.current?.focus(),
clear: () => { if (inner.current) inner.current.value = ""; },
}));
return <input ref={inner} defaultValue={props.initial} />;
});
// Use:
const ref = useRef<Handle>(null);
ref.current?.focus();
Use sparingly — usually a sign you’re fighting React’s data flow.
Combining multiple refs
function mergeRefs<T>(...refs: React.Ref<T>[]): React.RefCallback<T> {
return (value) => {
refs.forEach((r) => {
if (typeof r === "function") r(value);
else if (r) (r as any).current = value;
});
};
}
<input ref={mergeRefs(ref1, ref2)} />
useRef vs useState
| Aspect | useState | useRef |
|---|---|---|
| Trigger render? | Yes | No |
| Read in render? | Yes | Avoid |
| Mutable? | Replace only | Mutate freely |
Rule: if the UI needs to update on change → state. Otherwise → ref.
Reading refs during render
function Comp() {
const ref = useRef(0);
ref.current; // BAD: undefined behavior in concurrent rendering
return <div />;
}
Read refs in effects, event handlers, or callbacks. Never in render.
Refs and SSR
useEffect(() => {
// ref.current is set here (client only)
}, []);
ref.current is undefined on server; only set after mount. Wrap browser-only access.
DOM measurement pattern
const [width, setWidth] = useState(0);
const ref = useCallback((node: HTMLDivElement | null) => {
if (node) setWidth(node.offsetWidth);
}, []);
useEffect(() => {
if (!ref.current) return;
const ro = new ResizeObserver(([entry]) => {
setWidth(entry.contentRect.width);
});
ro.observe(ref.current);
return () => ro.disconnect();
}, []);
Forwarding HTML attributes
const Input = forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
(props, ref) => <input {...props} ref={ref} />
);
Common mistakes
- Using ref when state would be cleaner.
- Reading
ref.currentduring render. - Forgetting
nullinit for DOM refs. - Spreading ref onto a component that doesn’t forward — silent failure.
useImperativeHandlewhen an event/prop would suffice.
Read this next
If you want my refs + measurement utilities, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .