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.current is 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.current during 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.Letting the React Compiler do it
React Compiler reached 1.0 in October 2025. It memoizes your code for you: at build time it analyzes each component and inserts the equivalent of useMemo, useCallback, and memo where they actually help. You write plain code and stop hand-placing dependency arrays.
It is still opt-in rather than on by default. In Next.js you install the Babel plugin and flip one config flag:
// pnpm add -D babel-plugin-react-compiler
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;Next.js narrows the work with an SWC pass that only hands the compiler files containing JSX or hooks, so builds stay close to normal speed. The compiler-powered lint rules ship in eslint-plugin-react-hooks, which will tell you when a component breaks the rules it depends on.
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.