본문으로 건너뛰기 React useMemo and useCallback: When They Pay Off

React useMemo and useCallback: When They Pay Off

React useMemo and useCallback: When They Pay Off

이 글의 핵심

useMemo and useCallback in React: reference stability and expensive work — when to use them, how to avoid premature optimization, and how to verify with Profiler.

Why Re-renders Happen

In React, a component re-renders when its state changes, its props change, or its parent re-renders. For most components, this is fine — renders are usually fast. The problem arises when:

  1. A computation inside the component is genuinely expensive (filtering 100k items, complex math)
  2. A new function or object reference is created on every render and passed to a memoized child — breaking that child’s memoization

useMemo and useCallback are tools to address these two specific cases. They are not a general-purpose performance boost.


useMemo: Memoize an Expensive Value

useMemo runs a function and caches the result. It only re-runs when the dependencies change:

import { useMemo, useState } from 'react';

type Item = { id: string; label: string; score: number };

function Leaderboard({ items }: { items: Item[] }) {
    const [query, setQuery] = useState('');
    const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');

    // Without useMemo: runs on every render, even when only sortDir changes
    // With useMemo: only re-runs when items or query changes
    const filtered = useMemo(() => {
        const q = query.trim().toLowerCase();
        if (!q) return items;
        return items.filter(item => item.label.toLowerCase().includes(q));
    }, [items, query]);  // dependencies

    // Second useMemo: only re-runs when filtered or sortDir changes
    const sorted = useMemo(() => {
        return [...filtered].sort((a, b) =>
            sortDir === 'desc' ? b.score - a.score : a.score - b.score
        );
    }, [filtered, sortDir]);

    return (
        <section>
            <input
                value={query}
                onChange={e => setQuery(e.target.value)}
                placeholder="Search..."
            />
            <button onClick={() => setSortDir(d => d === 'asc' ? 'desc' : 'asc')}>
                Sort {sortDir === 'desc' ? '↑' : '↓'}
            </button>
            <ul>
                {sorted.map(item => (
                    <li key={item.id}>{item.label}{item.score}</li>
                ))}
            </ul>
        </section>
    );
}

When the user toggles sort direction, filtered does not re-run — only sorted does. This pays off when items is large and the filter is slow.


useCallback: Stabilize a Function Reference

Every render creates new function objects. This matters when the function is passed to a child wrapped in React.memo — a new function reference breaks the memo comparison:

import { memo, useCallback, useState } from 'react';

// Wrapped in memo — only re-renders when props change by reference
const Row = memo(function Row({
    id,
    onSelect,
}: {
    id: string;
    onSelect: (id: string) => void;
}) {
    console.log(`Row ${id} rendered`);
    return <button onClick={() => onSelect(id)}>{id}</button>;
});

function Table({ ids }: { ids: string[] }) {
    const [active, setActive] = useState<string | null>(null);
    const [count, setCount] = useState(0);

    // Without useCallback: new function every render → Row always re-renders
    // With useCallback: same function reference → Row only re-renders if deps change
    const handleSelect = useCallback((id: string) => {
        setActive(id);
    }, []);  // no dependencies — never recreated

    return (
        <div>
            <p>Active: {active}</p>
            <button onClick={() => setCount(c => c + 1)}>
                Increment ({count})
            </button>
            {ids.map(id => (
                <Row key={id} id={id} onSelect={handleSelect} />
            ))}
        </div>
    );
}

When the user clicks “Increment”, count changes, Table re-renders — but handleSelect is the same reference, so Row does not re-render. Without useCallback, clicking increment would re-render all rows needlessly.

useCallback(fn, deps) is equivalent to useMemo(() => fn, deps). It’s just syntax sugar for functions.


The Two Cases Where Memoization Helps

SituationHookWhen it pays off
Expensive pure computationuseMemoWhen the computation is measurably slow (>1ms) and runs on renders that don’t change its inputs
Stable reference for memo’d childuseCallback or useMemoWhen the child is wrapped in React.memo and the function/object is a prop
Creating a stable object for a Context valueuseMemoWhen the Context value is an object created inline and consumed by many components

Context Value Stability

A common source of unnecessary re-renders: creating a new object in the Context Provider on every render:

// BAD: new object every render → all consumers re-render
function AppProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState<User | null>(null);
    const [theme, setTheme] = useState<'light' | 'dark'>('light');

    return (
        <AppContext.Provider value={{ user, setUser, theme, setTheme }}>
            {children}
        </AppContext.Provider>
    );
}

// GOOD: stable object reference — consumers only re-render when user or theme changes
function AppProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState<User | null>(null);
    const [theme, setTheme] = useState<'light' | 'dark'>('light');

    const value = useMemo(() => ({ user, setUser, theme, setTheme }), [user, theme]);

    return (
        <AppContext.Provider value={value}>
            {children}
        </AppContext.Provider>
    );
}

Or split the context into fast-changing and slow-changing parts:

// Fast-changing: theme, locale
const UIContext = createContext<UIState>(...);

// Slow-changing: auth state, user profile
const AuthContext = createContext<AuthState>(...);

// Components that only care about theme don't re-render on auth changes

Measuring with React DevTools Profiler

Never add useMemo/useCallback without measuring first. Use the React DevTools Profiler:

  1. Open browser DevTools → React tab → Profiler
  2. Click Record, interact with your component, click Stop
  3. Look for components with long render times (orange/red bars)
  4. Check the “Why did this render?” panel — if a component re-renders with “hook changed” on every parent render, useCallback might help

Typical findings:

  • Most components render in < 1ms — memoization adds more overhead than it saves
  • A few components with large lists or complex computations benefit significantly
  • Leaf components wrapped in memo benefit from stable callback references

When NOT to Use These Hooks

Cheap computations: a simple arithmetic expression, array slice, string concatenation — memoizing these is slower than just recomputing. The useMemo call itself has overhead.

Components that always re-render anyway: if a component renders when its state changes, and that state is the input to the useMemo, you’re not saving anything.

Non-memo children: useCallback only helps when the child uses React.memo (or a dependency array like useEffect). If the child always re-renders regardless, the stable reference doesn’t matter.

// No benefit: Child doesn't use memo, so it re-renders on every parent render anyway
function Parent() {
    const fn = useCallback(() => doSomething(), []);  // unnecessary
    return <Child onAction={fn} />;  // Child will re-render anyway
}

// Benefit: Child uses memo — the stable reference prevents unnecessary renders
const Child = memo(({ onAction }) => <button onClick={onAction}>Click</button>);

Dependency Array Gotchas

Stale closure: forgetting a dependency means the memoized value or callback captures an old value:

// BUG: userId is used inside but not listed as a dependency
const fetchUser = useCallback(async () => {
    const data = await api.get(`/users/${userId}`);  // always uses initial userId
    setUser(data);
}, []);  // should be [userId]

// CORRECT
const fetchUser = useCallback(async () => {
    const data = await api.get(`/users/${userId}`);
    setUser(data);
}, [userId]);  // re-created when userId changes

Use the eslint-plugin-react-hooks rule react-hooks/exhaustive-deps to catch missing dependencies automatically.

Object/array in dependencies: objects are compared by reference. If a parent creates options = { limit: 10 } inline, it’s a new object every render — which defeats the memoization:

// BAD: options is a new object every render → useMemo always re-runs
const results = useMemo(() => filter(data, options), [data, options]);

// GOOD: either memoize options separately, or list specific values
const results = useMemo(() => filter(data, { limit: 10 }), [data]);
// Or:
const { limit, offset } = options;
const results = useMemo(() => filter(data, { limit, offset }), [data, limit, offset]);

Practical Checklist

Before adding useMemo or useCallback, answer:

  1. Did you measure? Open the Profiler and confirm the component renders unnecessarily or slowly.
  2. Is the computation expensive? Filtering 10k items: yes. Adding two numbers: no.
  3. Is the child memoized? useCallback only helps if the child is wrapped in React.memo.
  4. Are all dependencies listed? Run react-hooks/exhaustive-deps and fix all warnings.
  5. Does it actually help? Re-measure with the Profiler after adding the hook.

Key Takeaways

  • useMemo caches an expensive computed value — re-runs only when dependencies change
  • useCallback caches a function reference — prevents re-creating it on every render
  • Both hooks help in two specific cases: expensive computation, or stable reference for React.memo children
  • useCallback(fn, deps) is syntactic sugar for useMemo(() => fn, deps)
  • Measure first with the Profiler — don’t add these hooks speculatively
  • Context providers should wrap the value object in useMemo to prevent all consumers from re-rendering on unrelated state changes
  • Missing dependencies cause stale closures; use eslint-plugin-react-hooks to catch them

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. useMemo and useCallback in React: reference stability and expensive work — when to use them, how to avoid premature.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers React, useMemo, useCallback, Performance, Rendering, Memoization, Hooks.