useMemo и useCallback
useMemo
Заголовок раздела «useMemo»Мемоизирует результат вычисления — пересчитывает только если изменились зависимости.
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>;}Когда useMemo нужен
Заголовок раздела «Когда useMemo нужен»// 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]);Когда useMemo НЕ нужен
Заголовок раздела «Когда useMemo НЕ нужен»// ❌ Простые вычисления — overhead мемоизации > экономияconst doubled = useMemo(() => count * 2, [count]); // лишнее
// ✅ Просто вычислиconst doubled = count * 2;
// ❌ Каждый рендер компонента и так создаёт новые примитивы — мемоизация ничего не даётconst message = useMemo(() => `Привет, ${name}!`, [name]); // лишнееconst message = `Привет, ${name}!`; // ✅useCallback
Заголовок раздела «useCallback»Мемоизирует функцию — возвращает одну и ту же ссылку если зависимости не изменились.
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>;});useCallback с зависимостями
Заголовок раздела «useCallback с зависимостями»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> );}Связь useMemo и useCallback
Заголовок раздела «Связь useMemo и useCallback»// useCallback — это useMemo для функцийconst memoizedFn = useCallback(fn, deps);// эквивалентно:const memoizedFn = useMemo(() => fn, deps);Ловушки мемоизации
Заголовок раздела «Ловушки мемоизации»Ловушка 1: нарушение пользы React.memo
Заголовок раздела «Ловушка 1: нарушение пользы React.memo»// ❌ useCallback бесполезен без React.memo у получателяconst handleClick = useCallback(() => setCount(c => c + 1), []);
// Обычный (не мемоизированный) компонент всё равно рендерится при каждом рендере родителяfunction RegularChild({ onClick }: { onClick: () => void }) { return <button onClick={onClick}>Click</button>;}// → useCallback ничего не даёт
// ✅ useCallback полезен только с React.memoconst MemoChild = React.memo(function MemoChild({ onClick }: { onClick: () => void }) { return <button onClick={onClick}>Click</button>;});Ловушка 2: объекты внутри useMemo
Заголовок раздела «Ловушка 2: объекты внутри useMemo»// ❌ Внутри useMemo создаём новый массив как зависимость другого useMemoconst 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 — новый массив при каждом рендере родителя (если не мемоизирован там)}Ловушка 3: stale closure в useCallback
Заголовок раздела «Ловушка 3: stale closure в useCallback»// ❌ Слишком агрессивное кэширование — пропускаем нужные зависимости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> );}