REACT & NEXT.JS / 3. USEREF, USEMEMO, USECALLBACK

useRef, useMemo & useCallback

Performance hooks — avoid unnecessary renders and recomputation


EXPLANATION

useRef: a mutable container that persists across renders without causing re-renders.
Two uses:
1. DOM reference → ref.current points to the actual DOM element
2. Mutable value that shouldn't trigger re-render (e.g. storing previous value, timers)

useMemo: memoizes an expensive computed value.
const result = useMemo(() => expensiveCalc(a, b), [a, b])
Only recomputes when a or b changes. For everything else, returns cached value.

useCallback: memoizes a function reference.
const fn = useCallback(() => doSomething(x), [x])
Without this, a new function object is created on every render, which breaks React.memo and causes child re-renders.

When to use:
• useMemo → expensive computations (filtering large lists, complex transforms)
• useCallback → functions passed as props to memoized child components
• useRef → DOM manipulation, storing timers, previous values

When NOT to use: don't add these everywhere "for performance". They add overhead. Profile first, optimize second.

DIAGRAM

Without useCallback:
  Parent renders → new onClick function created
  → Child sees new prop reference
  → Child re-renders (even if React.memo!)

  With useCallback:
  Parent renders → same onClick reference returned (if deps unchanged)
  → Child sees same prop reference
  → Child skips re-render ✓

  useMemo example:
  [1000 items] → filter → sort → format
  Without useMemo: runs every render (even unrelated state changes)
  With useMemo([items, filter]): only runs when items or filter changes

CODE

TSX
1"use client";
2
3import { useState, useRef, useMemo, useCallback, memo } from "react";
4
5// ── useRef: DOM access ────────────────────────────────────────────
6export function AutoFocusInput() {
7 const inputRef = useRef<HTMLInputElement>(null);
8
9 const focusInput = () => {
10 inputRef.current?.focus(); // direct DOM access
11 inputRef.current?.select();
12 };
13
14 return (
15 <div>
16 <input ref={inputRef} placeholder="Click button to focus me" />
17 <button onClick={focusInput}>Focus</button>
18 </div>
19 );
20}
21
22// ── useRef: store timer without re-rendering ───────────────────────
23export function Stopwatch() {
24 const [time, setTime] = useState(0);
25 const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
26
27 const start = () => {
28 if (intervalRef.current) return;
29 intervalRef.current = setInterval(() => {
30 setTime(prev => prev + 1);
31 }, 1000);
32 };
33
34 const stop = () => {
35 if (intervalRef.current) {
36 clearInterval(intervalRef.current);
37 intervalRef.current = null;
38 }
39 };
40
41 return (
42 <div>
43 <p>{time}s</p>
44 <button onClick={start}>Start</button>
45 <button onClick={stop}>Stop</button>
46 </div>
47 );
48}
49
50// ── useMemo: expensive filtering ─────────────────────────────────
51export function FilteredList({ items }: { items: { id: number; name: string; score: number }[] }) {
52 const [query, setQuery] = useState("");
53 const [theme, setTheme] = useState("dark"); // unrelated state
54
55 // Without useMemo: re-filters on EVERY render, including theme changes
56 // With useMemo: only re-filters when items or query changes
57 const filtered = useMemo(() => {
58 console.log("Filtering..."); // see when this runs
59 return items
60 .filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
61 .sort((a, b) => b.score - a.score);
62 }, [items, query]);
63
64 return (
65 <div>
66 <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Search..." />
67 <button onClick={() => setTheme(t => t === "dark" ? "light" : "dark")}>
68 Toggle theme (doesn't re-filter)
69 </button>
70 <ul>{filtered.map(item => <li key={item.id}>{item.name}: {item.score}</li>)}</ul>
71 </div>
72 );
73}
74
75// ── useCallback + memo: prevent child re-renders ──────────────────
76const ExpensiveChild = memo(function ExpensiveChild({
77 onAction
78}: {
79 onAction: (id: number) => void
80}) {
81 console.log("Child rendered");
82 return <button onClick={() => onAction(1)}>Action</button>;
83});
84
85export function Parent() {
86 const [count, setCount] = useState(0);
87 const [data, setData] = useState<number[]>([]);
88
89 // Without useCallback: new function on every render child always re-renders
90 // With useCallback: same reference child skips re-render when count changes
91 const handleAction = useCallback((id: number) => {
92 setData(prev => [...prev, id]);
93 }, []); // no deps never recreated
94
95 return (
96 <div>
97 <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
98 <ExpensiveChild onAction={handleAction} />
99 </div>
100 );
101}
← PREV2. useState & useEffectNEXT →4. Context & State Management