Как удалить последний элемент массива в setState без мутаций в React?
Что проверяют
Понимаете ли вы, почему React ждёт новый массив, а не изменённый старый, и какие методы массива мутируют исходник. Следом часто спрашивают про функциональную форму обновления и про немутирующие методы из ES2023.
Как отвечать
useState сравнивает старое и новое значение через Object.is. Если сделать items.pop() и передать тот же массив, ссылка не изменится, и React пропустит перерисовку. Классовый setState перерисует, но PureComponent и memo дочерних компонентов увидят ту же ссылку и не обновятся.
Рабочие варианты:
slice(0, -1): новый массив без последнего элемента, исходный не тронут. Классика;toSpliced(-1, 1): немутирующая версияspliceиз ES2023, есть во всех актуальных браузерах;filter((_, i) => i !== arr.length - 1): работает, но проходит весь массив ради одного элемента;with,toSorted,toReversed: той же серии, пригодятся для замены по индексу и сортировки без ручного копирования.
Главное: брать предыдущее значение из аргумента апдейтера, а не из замыкания.
setItems(prev => prev.slice(0, -1));Если вызвать обновление дважды в одном обработчике или из setTimeout, вариант setItems(items.slice(0, -1)) возьмёт устаревшее items из своего рендера, и удалится один элемент вместо двух. В классовом компоненте то же самое: this.setState(s => ({ items: s.items.slice(0, -1) })).
На пустом массиве slice(0, -1) вернёт пустой массив без ошибки. Лишний рендер можно не делать, вернув prev как есть.
Где спотыкаются
Мутируют внутри апдейтера: prev.pop(); return [...prev]. В Strict Mode React в разработке вызывает апдейтер дважды с одним и тем же prev, и пропадают два элемента.
Путают поверхностную копию с глубокой. [...items] это новый массив, но объекты внутри те же, и copy[0].done = true снова мутирует состояние.
Считают, что const current = items создаёт копию. Это та же ссылка, и любая операция над ней меняет состояние напрямую.