RE

useMemo & useCallback

React · 6 entries

useMemo

syntax
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
example
import { useMemo, useState } from 'react';

function ProductFilter({ products }) {
  const [query, setQuery] = useState('');
  const [sortBy, setSortBy] = useState('name');

  const filtered = useMemo(() => {
    const matched = products.filter(p =>
      p.name.toLowerCase().includes(query.toLowerCase())
    );
    return matched.sort((a, b) =>
      a[sortBy] > b[sortBy] ? 1 : -1
    );
  }, [products, query, sortBy]);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <select value={sortBy} onChange={e => setSortBy(e.target.value)}>
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
      <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
    </div>
  );
}

Note useMemo caches the result of a computation. React may discard the cache under memory pressure, so never rely on it for correctness -- only for performance. Do not memoize everything by default; profile first.

useCallback

syntax
const memoizedFn = useCallback((args) => {
  doSomething(args, dep);
}, [dep]);
example
import { useCallback, useState } from 'react';

function ParentList({ items }) {
  const [selectedId, setSelectedId] = useState(null);

  const handleSelect = useCallback((id) => {
    setSelectedId(id);
  }, []);

  return (
    <ul>
      {items.map(item => (
        <ListItem
          key={item.id}
          item={item}
          onSelect={handleSelect}
          isSelected={item.id === selectedId}
        />
      ))}
    </ul>
  );
}

Note useCallback(fn, deps) is identical to useMemo(() => fn, deps). Its primary use case is passing stable function references to memoized child components (wrapped with React.memo) to prevent their re-renders.

Dependency Arrays

syntax
// All values from component scope used inside must be listed
useCallback(() => fn(a, b), [a, b]);
useMemo(() => compute(x), [x]);
example
function ChatRoom({ roomId, serverUrl }) {
  // Recreated only when roomId or serverUrl change
  const connect = useCallback(() => {
    const ws = new WebSocket(`${serverUrl}/rooms/${roomId}`);
    ws.onopen = () => console.log('Connected to', roomId);
    return ws;
  }, [roomId, serverUrl]);

  useEffect(() => {
    const ws = connect();
    return () => ws.close();
  }, [connect]);

  return <div>Chat: {roomId}</div>;
}

Note Missing a dependency leads to stale closures. Including an unnecessary one causes pointless recalculations. Use the exhaustive-deps lint rule (eslint-plugin-react-hooks) to catch mistakes automatically.

React.memo

syntax
const MemoizedComponent = React.memo(Component);
// With custom comparison
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id;
});
example
import { memo, useState, useCallback } from 'react';

const ExpensiveRow = memo(function ExpensiveRow({ item, onToggle }) {
  // This component only re-renders if item or onToggle change
  return (
    <tr onClick={() => onToggle(item.id)}>
      <td>{item.name}</td>
      <td>{item.value.toFixed(2)}</td>
    </tr>
  );
});

function DataTable({ rows }) {
  const [selected, setSelected] = useState(new Set());

  const handleToggle = useCallback((id) => {
    setSelected(prev => {
      const next = new Set(prev);
      next.has(id) ? next.delete(id) : next.add(id);
      return next;
    });
  }, []);

  return (
    <table>
      <tbody>
        {rows.map(row => (
          <ExpensiveRow key={row.id} item={row} onToggle={handleToggle} />
        ))}
      </tbody>
    </table>
  );
}

Note React.memo does a shallow comparison of props. It only helps if the component re-renders with the same props frequently. Pair it with useCallback for function props and useMemo for object/array props.

When to Memoize

syntax
// DO memoize when:
// - Computation is genuinely expensive (sorting large arrays, complex math)
// - Passing callbacks/objects to React.memo children
// - Value is a dependency of another hook

// DON'T memoize when:
// - The computation is trivial
// - The component rarely re-renders
// - No downstream consumer cares about reference stability
example
// GOOD: expensive computation
const sortedTransactions = useMemo(
  () => transactions.slice().sort(compareDates),
  [transactions]
);

// GOOD: stable reference for memoized child
const handleDelete = useCallback(
  (id) => dispatch({ type: 'DELETE', id }),
  [dispatch]
);

// UNNECESSARY: trivial computation
// const label = useMemo(() => `Hello ${name}`, [name]);
// Just write: const label = `Hello ${name}`;

Note Memoization has a cost: it uses memory to store results and adds comparison overhead. Only apply it where profiling shows a measurable benefit. Premature memoization adds complexity without improving performance.

Stable References for Effects

syntax
const options = useMemo(() => ({ key: value }), [value]);
useEffect(() => { doSomething(options); }, [options]);
example
function DataFetcher({ endpoint, filters }) {
  // Without useMemo, a new object is created every render,
  // causing the effect to re-run endlessly
  const requestConfig = useMemo(
    () => ({
      url: endpoint,
      params: filters,
      headers: { 'Accept': 'application/json' },
    }),
    [endpoint, filters]
  );

  useEffect(() => {
    const controller = new AbortController();
    fetch(requestConfig.url, {
      ...requestConfig,
      signal: controller.signal,
    }).then(r => r.json()).then(setData);

    return () => controller.abort();
  }, [requestConfig]);
}

Note When an object or array is a dependency of useEffect, memoize it to prevent infinite effect loops. Every render creates a new reference, and useEffect sees a 'change' even when the values inside are identical.