Skip to main content

Memoization

Memoization is a technique to cache computed values and skip unnecessary re-renders, improving performance by avoiding redundant work.

React.memo

React.memo is a higher-order component that prevents re-renders when props haven’t changed. It performs a shallow comparison of props (see ReactMemo.js:12).

Implementation

From ReactMemo.js:12-29, React.memo wraps a component and optionally accepts a custom comparison function:
// Simplified from ReactMemo.js
export function memo<Props>(
  type: React$ElementType,
  compare?: (oldProps: Props, newProps: Props) => boolean,
) {
  const elementType = {
    $$typeof: REACT_MEMO_TYPE,
    type,
    compare: compare === undefined ? null : compare,
  };
  return elementType;
}

Basic Usage

import { memo } from 'react';

function UserCard({ name, email, avatar }) {
  console.log('UserCard rendered');
  return (
    <div>
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

// UserCard only re-renders when name, email, or avatar change
export default memo(UserCard);

Before: Unnecessary Re-renders

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'Alice', email: 'alice@example.com' });
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      {/* UserCard re-renders every time count changes */}
      <UserCard name={user.name} email={user.email} />
    </div>
  );
}

After: Optimized with memo

const UserCard = memo(function UserCard({ name, email }) {
  console.log('UserCard rendered');
  return (
    <div>
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'Alice', email: 'alice@example.com' });
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      {/* UserCard now skips re-renders when count changes */}
      <UserCard name={user.name} email={user.email} />
    </div>
  );
}
Result: When clicking the button, only the parent re-renders. UserCard stays unchanged because its props haven’t changed.

Custom Comparison Function

By default, React.memo does shallow comparison. Provide a custom comparison for complex scenarios:
function UserCard({ user, settings }) {
  return <div>{user.name}</div>;
}

// Custom comparison: only re-render if user.name changes
export default memo(UserCard, (prevProps, nextProps) => {
  // Return true if props are equal (skip render)
  // Return false if props are different (re-render)
  return prevProps.user.name === nextProps.user.name;
});
The custom comparison function works opposite to shouldComponentUpdate. Return true to skip rendering, false to re-render.

Display Name Handling

From ReactMemo.js:31-56, React automatically handles display names for debugging:
// Anonymous function gets the display name
const MemoizedComponent = memo((props) => {
  return <div>{props.content}</div>;
});

MemoizedComponent.displayName = 'MyComponent';
// Now shows as "MyComponent" in React DevTools

useMemo

useMemo caches expensive calculations, recomputing only when dependencies change (see ReactHooks.js:143).

Implementation

From ReactHooks.js:143-149:
export function useMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

Basic Usage

import { useMemo } from 'react';

function ProductList({ products, filter }) {
  // Expensive calculation only runs when products or filter change
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products
      .filter(p => p.category === filter)
      .sort((a, b) => b.rating - a.rating);
  }, [products, filter]);
  
  return (
    <ul>
      {filteredProducts.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Before: Re-computing on Every Render

function DataDashboard({ data }) {
  const [sortOrder, setSortOrder] = useState('asc');
  
  // This calculation runs on EVERY render, even if data doesn't change
  const processedData = data
    .map(item => ({ ...item, computed: expensiveCalculation(item) }))
    .sort((a, b) => sortOrder === 'asc' ? a.value - b.value : b.value - a.value);
  
  return (
    <div>
      <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
        Toggle Sort
      </button>
      <DataTable data={processedData} />
    </div>
  );
}

After: Optimized with useMemo

function DataDashboard({ data }) {
  const [sortOrder, setSortOrder] = useState('asc');
  
  // Expensive calculation cached - only re-runs when data or sortOrder change
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return data
      .map(item => ({ ...item, computed: expensiveCalculation(item) }))
      .sort((a, b) => sortOrder === 'asc' ? a.value - b.value : b.value - a.value);
  }, [data, sortOrder]);
  
  return (
    <div>
      <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
        Toggle Sort
      </button>
      <DataTable data={processedData} />
    </div>
  );
}
Result: The expensive calculation only runs when data or sortOrder actually changes, not on every render.

Referential Equality

Use useMemo to maintain stable object references:
function UserProfile({ userId, theme }) {
  // Without useMemo: new object on every render
  // const config = { userId, theme, timestamp: Date.now() };
  
  // With useMemo: same object reference if dependencies don't change
  const config = useMemo(
    () => ({ userId, theme, timestamp: Date.now() }),
    [userId, theme]
  );
  
  // UserSettings won't re-render unnecessarily
  return <UserSettings config={config} />;
}

const UserSettings = memo(function UserSettings({ config }) {
  return <div>{config.userId}</div>;
});

useCallback

useCallback memoizes functions, ensuring the same function reference across renders (see ReactHooks.js:135).

Implementation

From ReactHooks.js:135-141:
export function useCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, deps);
}

Basic Usage

import { useCallback } from 'react';

function TodoList({ todos }) {
  const [filter, setFilter] = useState('all');
  
  // Function reference stays the same unless filter changes
  const handleToggle = useCallback((id) => {
    console.log(`Toggle todo ${id} with filter ${filter}`);
    // Toggle logic here
  }, [filter]);
  
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={handleToggle}
        />
      ))}
    </ul>
  );
}

const TodoItem = memo(function TodoItem({ todo, onToggle }) {
  return (
    <li onClick={() => onToggle(todo.id)}>
      {todo.text}
    </li>
  );
});

Before: New Function on Every Render

function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');
  
  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
      />
      {/* New function created on every render */}
      <SearchResults 
        onResultClick={(id) => {
          console.log('Clicked:', id);
          onSearch(id);
        }}
      />
    </div>
  );
}

const SearchResults = memo(function SearchResults({ onResultClick }) {
  // Re-renders on every parent render because onResultClick changes
  return <div>Results...</div>;
});

After: Stable Function Reference

function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('');
  
  // Function reference stays stable across renders
  const handleResultClick = useCallback((id) => {
    console.log('Clicked:', id);
    onSearch(id);
  }, [onSearch]);
  
  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
      />
      {/* SearchResults won't re-render unnecessarily */}
      <SearchResults onResultClick={handleResultClick} />
    </div>
  );
}

const SearchResults = memo(function SearchResults({ onResultClick }) {
  // Only re-renders when onResultClick actually changes
  return <div>Results...</div>;
});
Result: SearchResults doesn’t re-render when typing in the input field because handleResultClick maintains the same reference.

useCallback vs useMemo

useCallback is a shorthand for useMemo that returns a function:
// These are equivalent:
const handleClick = useCallback(() => {
  console.log('clicked');
}, []);

const handleClick = useMemo(() => {
  return () => console.log('clicked');
}, []);

When to Memoize

Memoization is useful when:
  • Component renders frequently with the same props
  • Component has expensive calculations or rendering
  • Passing callbacks to memoized child components
  • Working with large lists or complex data structures
  • Preventing effect re-runs with stable dependencies

Good Candidates for Memoization

// ✅ Expensive calculation
const sortedData = useMemo(
  () => largeArray.sort().filter(complexCondition),
  [largeArray]
);

// ✅ Frequently rendered with same props
const ListItem = memo(function ListItem({ item }) {
  return <ExpensiveComponent data={item} />;
});

// ✅ Callback passed to memoized child
const handleClick = useCallback(
  (id) => updateItem(id),
  [updateItem]
);

// ✅ Complex object used in effects
const config = useMemo(
  () => ({ endpoint, headers, retry: 3 }),
  [endpoint, headers]
);

Poor Candidates for Memoization

// ❌ Simple calculation (memoization overhead not worth it)
const doubled = useMemo(() => count * 2, [count]);

// ❌ Component that rarely renders
const RarelyRendered = memo(function RarelyRendered({ data }) {
  return <div>{data}</div>;
});

// ❌ Props change on every render anyway
const timestamped = useMemo(
  () => ({ data, timestamp: Date.now() }),
  [data]
);

// ❌ Callback not passed to memoized components
const handleClick = useCallback(
  () => console.log('click'),
  []
);
// If not passed to memo'd children, regular function is fine
Avoid premature optimization! Memoization has costs:
  • Memory overhead to store cached values
  • Comparison overhead on every render
  • Code complexity and maintenance burden
Only memoize when profiling shows a real performance problem. In many cases, React’s default behavior is fast enough.

Memoization Trade-offs

Memory vs Speed

function DataGrid({ data }) {
  // Trades memory for speed
  const processedRows = useMemo(() => {
    // This result is cached in memory
    return data.map(row => expensiveTransform(row));
  }, [data]);
  
  // Consider: Is the memory cost worth it?
  // If data is large and changes frequently, maybe not
}

Comparison Overhead

// Shallow comparison is fast
const MemoizedSimple = memo(function Simple({ name, age }) {
  return <div>{name}</div>;
});

// Deep comparison is expensive - avoid if possible
const MemoizedComplex = memo(
  function Complex({ deeply, nested, object }) {
    return <div>{deeply.nested.object.value}</div>;
  },
  (prev, next) => {
    // This comparison runs on every render
    return isDeepEqual(prev, next); // Expensive!
  }
);

Dependency Management

function SearchComponent({ items, filters }) {
  // Dependencies must be complete and stable
  const filtered = useMemo(() => {
    return items.filter(item => {
      // If you use filters.category but don't list it in deps,
      // you'll have stale data
      return item.category === filters.category;
    });
  }, [items, filters.category]); // ✅ Correct dependencies
  
  // ❌ Wrong: missing dependency
  // }, [items]);
}
Use the eslint-plugin-react-hooks ESLint plugin to catch missing dependencies. It warns when your dependency array might be incorrect.

Real-World Patterns

Combining Memoization Techniques

From ReactProfiler-test.internal.js:204, here’s a pattern combining React.memo with useMemo:
const DataTable = memo(function DataTable({ data, sortKey }) {
  // Component only re-renders when props change (memo)
  // AND computation only re-runs when dependencies change (useMemo)
  const sortedData = useMemo(
    () => [...data].sort((a, b) => a[sortKey] - b[sortKey]),
    [data, sortKey]
  );
  
  return (
    <table>
      {sortedData.map(row => (
        <TableRow key={row.id} data={row} />
      ))}
    </table>
  );
});

const TableRow = memo(function TableRow({ data }) {
  // Each row only re-renders when its data changes
  return (
    <tr>
      <td>{data.name}</td>
      <td>{data.value}</td>
    </tr>
  );
});

Memoizing Context Values

const UserContext = createContext();

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  // Without useMemo, all consumers re-render on every parent render
  const value = useMemo(
    () => ({ user, setUser }),
    [user]
  );
  
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

Optimizing Event Handlers

function Form({ onSubmit }) {
  const [values, setValues] = useState({ name: '', email: '' });
  
  // Stable references for form field callbacks
  const handleNameChange = useCallback(
    (e) => setValues(v => ({ ...v, name: e.target.value })),
    []
  );
  
  const handleEmailChange = useCallback(
    (e) => setValues(v => ({ ...v, email: e.target.value })),
    []
  );
  
  const handleSubmit = useCallback(
    (e) => {
      e.preventDefault();
      onSubmit(values);
    },
    [values, onSubmit]
  );
  
  return (
    <form onSubmit={handleSubmit}>
      <MemoizedInput value={values.name} onChange={handleNameChange} />
      <MemoizedInput value={values.email} onChange={handleEmailChange} />
      <button type="submit">Submit</button>
    </form>
  );
}

const MemoizedInput = memo(function Input({ value, onChange }) {
  return <input value={value} onChange={onChange} />;
});

Debugging Memoization

Check if memoization works:

const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
  console.log('ExpensiveComponent rendered');
  return <div>{data}</div>;
});

// In parent component:
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveComponent data="static" />
    </div>
  );
}

// Click the button multiple times
// If memoization works: "ExpensiveComponent rendered" logs only once
// If it doesn't work: logs on every click

Using React DevTools

React DevTools Profiler shows why components re-rendered:
  1. Open React DevTools
  2. Go to Profiler tab
  3. Record a session
  4. Click on a component in the flame graph
  5. Check “Why did this render?” to see which props changed