Перейти к содержимому

useMemo и useCallback

Мемоизирует результат вычисления — пересчитывает только если изменились зависимости.

import { useMemo } from 'react';
function FilteredList({ items, query }: { items: Product[]; query: string }) {
// Без useMemo: фильтрация выполняется на каждом рендере
// С useMemo: только когда items или query изменились
const filtered = useMemo(
() => items.filter(p => p.name.toLowerCase().includes(query.toLowerCase())),
[items, query]
);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
// 1. Дорогие вычисления
const sortedData = useMemo(
() => heavySort(largeArray), // O(n log n) с n = 10000+
[largeArray]
);
// 2. Создание объектов/массивов передаваемых в React.memo компоненты
const config = useMemo(
() => ({ theme: 'dark', language: 'ru' }),
[] // константа — создаётся один раз
);
// Без memo: config — новый объект каждый рендер → Child рендерится всегда
// 3. Передача в useEffect как зависимость
const params = useMemo(() => ({ page, limit }), [page, limit]);
useEffect(() => { fetchData(params); }, [params]);
// ❌ Простые вычисления — overhead мемоизации > экономия
const doubled = useMemo(() => count * 2, [count]); // лишнее
// ✅ Просто вычисли
const doubled = count * 2;
// ❌ Каждый рендер компонента и так создаёт новые примитивы — мемоизация ничего не даёт
const message = useMemo(() => `Привет, ${name}!`, [name]); // лишнее
const message = `Привет, ${name}!`; // ✅

Мемоизирует функцию — возвращает одну и ту же ссылку если зависимости не изменились.

import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// Без useCallback: новая функция каждый рендер → Child рендерится всегда
// С useCallback: та же функция если зависимости не изменились → Child не рендерится
const handleDelete = useCallback((id: number) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []); // нет зависимостей — функция никогда не меняется
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveList onDelete={handleDelete} />
</>
);
}
const ExpensiveList = React.memo(function ExpensiveList({
onDelete
}: {
onDelete: (id: number) => void;
}) {
console.log('ExpensiveList render'); // не вызывается при изменении count
return <div>...</div>;
});
function SearchComponent({ onSearch }: { onSearch: (q: string) => void }) {
const [value, setValue] = useState('');
// Функция меняется только когда onSearch меняется
const handleSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault();
onSearch(value);
},
[onSearch, value] // value нужен — используется внутри
);
return (
<form onSubmit={handleSubmit}>
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="submit">Найти</button>
</form>
);
}
// useCallback — это useMemo для функций
const memoizedFn = useCallback(fn, deps);
// эквивалентно:
const memoizedFn = useMemo(() => fn, deps);
// ❌ useCallback бесполезен без React.memo у получателя
const handleClick = useCallback(() => setCount(c => c + 1), []);
// Обычный (не мемоизированный) компонент всё равно рендерится при каждом рендере родителя
function RegularChild({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click</button>;
}
// → useCallback ничего не даёт
// ✅ useCallback полезен только с React.memo
const MemoChild = React.memo(function MemoChild({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click</button>;
});
// ❌ Внутри useMemo создаём новый массив как зависимость другого useMemo
const a = useMemo(() => [1, 2, 3], []); // создаётся один раз
const b = useMemo(() => a.map(x => x * 2), [a]); // пересчитывается только если a меняется ✅
// ❌ Но если a — props:
function Bad({ items }: { items: number[] }) {
const doubled = useMemo(() => items.map(x => x * 2), [items]);
// items — новый массив при каждом рендере родителя (если не мемоизирован там)
}
// ❌ Слишком агрессивное кэширование — пропускаем нужные зависимости
function StaleCounter() {
const [count, setCount] = useState(0);
// ESLint предупредит: 'count' отсутствует в зависимостях
const logCount = useCallback(() => {
console.log(count); // всегда 0!
}, []); // пустые зависимости — count заморожен
// ✅ Добавить в зависимости
const logCountFixed = useCallback(() => {
console.log(count);
}, [count]);
// ✅ Или использовать ref для доступа к актуальному значению
const countRef = useRef(count);
countRef.current = count;
const logCountWithRef = useCallback(() => {
console.log(countRef.current); // всегда актуальный
}, []);
}

Практический пример: оптимизация таблицы

Заголовок раздела «Практический пример: оптимизация таблицы»
interface Row {
id: number;
name: string;
value: number;
}
interface TableProps {
rows: Row[];
onRowClick: (id: number) => void;
}
// Дорогой компонент строки — мемоизируем
const TableRow = React.memo(function TableRow({
row,
onClick
}: {
row: Row;
onClick: (id: number) => void;
}) {
return (
<tr onClick={() => onClick(row.id)}>
<td>{row.name}</td>
<td>{row.value}</td>
</tr>
);
});
function DataTable({ rows, onRowClick }: TableProps) {
const [sortBy, setSortBy] = useState<keyof Row>('name');
// Мемоизируем сортировку — пересчёт только при изменении rows или sortBy
const sortedRows = useMemo(
() => [...rows].sort((a, b) => {
const av = a[sortBy], bv = b[sortBy];
return av < bv ? -1 : av > bv ? 1 : 0;
}),
[rows, sortBy]
);
// Мемоизируем обработчик — TableRow не рендерится при изменении sortBy
const handleClick = useCallback(onRowClick, [onRowClick]);
return (
<table>
<thead>
<tr>
<th onClick={() => setSortBy('name')}>Имя</th>
<th onClick={() => setSortBy('value')}>Значение</th>
</tr>
</thead>
<tbody>
{sortedRows.map(row => (
<TableRow key={row.id} row={row} onClick={handleClick} />
))}
</tbody>
</table>
);
}