Что происходит при инициализации массива в React-компоненте при каждом рендере и почему это плохо?
Что проверяют
Понимаете ли вы, что в React дорого на самом деле. Верный ответ не про аллокацию массива, а про ссылочное равенство: литерал в теле компонента каждый раз новый, и всё, что сравнивает ссылки, перестаёт работать.
Как отвечать
Создание маленького массива само по себе ничего не стоит и рендер не вызывает. Рендер вызывают состояние, контекст и рендер родителя. Проблемы начинаются там, где значение сравнивают по ссылке:
- массив в зависимостях
useEffect: эффект выполняется после каждого рендера, а если внутри естьsetState, получается цикл; - массив в пропсах дочернего компонента под
React.memo: мемоизация не срабатывает никогда; - значение контекста: перерисовываются все потребители;
getSnapshotвuseSyncExternalStore, возвращающий новый объект: бесконечный цикл.
Лечение выбирается по смыслу значения:
const TABS = ["all", "new"]; // константа, вынесена из компонента
function List({ items, query }) {
const visible = useMemo(
() => items.filter((i) => i.name.includes(query)),
[items, query] // зависимости из пропсов, а не из тела
);
}Неизменный список выносится за компонент, вычисляемое значение заворачивается в useMemo с честными зависимостями, тяжёлое начальное состояние задаётся ленивой формой useState(() => build()).
Где спотыкаются
Уверены, что новый массив сам по себе вызывает лишний рендер, и начинают мемоизировать всё подряд. Каждый useMemo это хранение и сравнение зависимостей, на простых значениях он обходится дороже пересоздания.
Второе: оборачивают в useMemo значение, у которого в зависимостях стоит литерал из того же тела компонента. Зависимость меняется каждый рендер, кэш не попадает ни разу, остаются одни накладные расходы.
Третье: путают ленивую инициализацию состояния с обычной. В записи useState(build()) функция зовётся на каждом рендере, а результат после первого просто отбрасывается; нужна форма с колбэком внутри.