useRef, useMemo, and useCallback
Escaping the render cycle, and memoising only when it actually pays for itself.
What you'll learn
- What a ref is, and the two things it is used for
- Caching an expensive calculation with
useMemo - Keeping a function's identity stable with
useCallback - Why memoising everything makes code slower to read and no faster to run
useRef: a value that survives renders
A ref is a box whose .current you can read and write. Two properties make it
useful:
- It persists across renders, like state.
- Changing it does not re-render, unlike state.
That combination is for values the UI does not display. Compare a ref with a plain variable and with state:
Click the first button several times, then force a re-render: the ref shows the true total, while the plain variable is back to zero. The ref remembered; it just did not ask for a render.
Never read or write a ref during rendering — that makes the render impure. Refs belong in event handlers and effects.
The other use: reaching a DOM node
Pass a ref to a JSX element and React puts the DOM node in .current:
Focusing, measuring, scrolling, and playing media are the legitimate cases. It is not a way to set values by hand — that is still state's job.
useMemo: cache a calculation
Every render re-runs the whole component body. Usually that is cheap. When it is
genuinely not, useMemo caches the result and only recomputes when its
dependencies change.
Imagine the function below is expensive — sorting thousands of rows, say. It logs whenever it actually runs. Type in the box: nothing is logged, because the memo returns the cached result. Click the number button and it recalculates.
Remove the useMemo and every keystroke recalculates. When the calculation is
genuinely slow, that is the difference between a responsive input and a janky
one — and it is a rarer situation than people assume.
useCallback: cache a function
Every render creates new function objects. Usually harmless — but it matters when a function is a dependency of an effect, or a prop to a memoised child, because identity comparison then sees a change every render.
useCallback returns the same function instance until its dependencies change.
It only pays off with React.memo, which skips re-rendering a child whose props
are unchanged:
Click "re-render parent" and watch the console: only unstable logs again.
memo compared the props, found a brand-new function, and re-rendered.
useMemo caches a value; useCallback caches a function. They are the
same idea — useCallback(fn, deps) is useMemo(() => fn, deps).
When not to memoise
Memoising is not free. It costs a dependency array to keep correct, it costs readability, and React still has to compare the dependencies on every render.
Reach for it when you have measured a problem, or when a value is a dependency of something that compares by identity. Otherwise leave it out. Re-rendering is usually fast, and premature memoisation adds bugs — a stale dependency array gives you a value that never updates, which is worse than a slow one.
Common mistakes
- Reading
ref.currentduring render. It makes render impure and will not update the screen anyway. - Using a ref instead of state. If the UI shows it, it is state.
useCallbackwithoutmemo. A stable function passed to a normal child changes nothing.- Memoising everything. More code, more dependencies to get wrong, no measured gain.
Exercise
The timer below cannot be stopped, because the interval id is stored in a plain variable that resets on every render. Move it into a ref so "Stop" works.