Refs, memo, useCallback
When to memoize. When NOT to. The React Compiler future.
Two corners of React that confuse everyone: refs and memoization. Refs are the escape hatch for "I need direct access to this DOM node" or "I need to remember a value without re-rendering." Memoization (memo, useMemo, useCallback) is the optimization escape hatch. Both are easy to overuse. Let's learn when they actually pay off.
useRef for DOM access
Sometimes you need the actual HTML element. Focus an input, measure a div, scroll an element into view. Pass a ref to its ref attribute and React fills it in with the DOM node:
function SearchInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}ref.currentis mutable. You can read and write it directly. Changes don't cause re-renders, which is the whole point. Refs are React's way of saying "this value exists, but don't make it part of the render cycle."
Refs as prop in React 19
Pre-React 19, you had to wrap your component in forwardRef to accept a ref from the outside. That's gone. Now ref is just a normal prop you can declare:
// React 18 and earlier
const MyInput = forwardRef(function MyInput(props, ref) {
return <input ref={ref} {...props} />;
});
// React 19+
function MyInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}Simpler. Cleaner. forwardRef still works for compatibility, but new code should just accept ref as a prop.
useRef for mutable values
Refs aren't just for DOM. Anytime you need a value that survives renders but shouldn't trigger them, use a ref. Common cases:
- An interval/timeout ID you need to clear later.
- A WebSocket or other long-lived connection.
- The previous value of a prop (for diffing).
- A "has mounted" flag to suppress initial-render logic.
function Timer() {
const intervalId = useRef(null);
function start() {
intervalId.current = setInterval(() => console.log("tick"), 1000);
}
function stop() {
clearInterval(intervalId.current);
}
return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}ref.currentduring render makes your component impure and breaks React features like Suspense and concurrent rendering. Treat ref as "state that lives outside React."React.memo: skip re-renders when props don't change
By default, when a parent re-renders, all its children re-render too, even if their props didn't change. memo wraps a component to skip the re-render when props are shallow-equal to the previous run.
const Row = memo(function Row({ item, onClick }) {
console.log("rendering", item.id);
return <li onClick={onClick}>{item.label}</li>;
});Sounds great. The catch: if any prop is a fresh object, array, or function on every parent render (which is normal), the shallow compare fails and memo does nothing. You then need useCallback for the function and useMemo for the object/array.
useMemo and useCallback
// useMemo caches a value
const sorted = useMemo(() => list.sort(), [list]);
// useCallback caches a function (equivalent to useMemo(() => fn, [...]))
const onClick = useCallback(() => doThing(id), [id]);Both take a function and a dependency array. They re-run only when a dep changes. Use cases:
- An expensive computation (sorting thousands of items, parsing) you don't want repeated every render.
- A function or object passed to a memoized child as a prop, so the child's shallow compare can succeed.
- A value used as a dep in a downstream
useEffect, so the effect doesn't fire on every render.
useMemo and useCallback add their own bookkeeping cost (storing deps, comparing them, holding old values). For a trivial calculation or a function passed to a regular (non-memo) child, they make code slower, not faster. Measure before optimizing.The React Compiler future
The React Compiler (shipped in 2025) memoizes your code automatically. When enabled, it analyzes your components at build time and inserts the equivalent of useMemo/useCallback/memo for you, only where it actually helps. The eventual end state: you write plain code, and React optimizes it.
For now, in 2026, the compiler is opt-in but stable in most Next.js and Vite setups. If you can enable it, do. You'll write less memoization noise. The rules: keep your components pure (no mutating props, no side effects during render) and the compiler does the rest.
Try it: a focus button and a memoized list
The first button uses a ref to focus the input. The list uses memo + useCallback to avoid re-rendering every row when only the counter changes. Open the console to watch the renders.
Quiz
What changes when you write ref.current = 5?
Recap
useRefstores a value that survives renders without causing them. Use for DOM access and mutable non-render state.- React 19 made
refa regular prop. No moreforwardRefneeded. React.memoskips re-renders when props are shallow equal. Often paired withuseCallback/useMemo.- Memoization has overhead. Don't reach for it until you have a measured problem.
- The React Compiler automates almost all of this. Enable it and write plain code.