Почему использование splice работает, а filter нет при обновлении состояния React на основе другой переменной состояния?
Я работаю с React и имею список секундомеров в состоянии. Когда я пытаюсь удалить элемент из списка на основе выбранного индекса, я заметил, что два разных подхода ведут себя по-разному:
Работающий подход
removeStopwatch={() => {
setStopwatches(s => {
const newArray = [...s];
newArray.splice(selectedStopwatchIndex, 1);
return newArray;
});
}}
Не работающий подход
removeStopwatch={() => {
setStopwatches(s => {
const newArray = [...s];
return newArray.filter((_, i) => i !== selectedStopwatchIndex);
});
}}
Первый подход с использованием splice работает правильно и удаляет выбранный секундомер. Однако второй подход с использованием filter не работает, хотя они кажутся функционально эквивалентными.
Я проверил, что selectedStopwatchIndex имеет правильное значение при нажатии кнопки удаления. Что я упускаю о том, как работают обновления состояния React в этом контексте?
Контекст кода
Вот релевантная структура компонента:
function MainScreen() {
const [stopwatches, setStopwatches] = useState([
new Stopwatch('A'),
new Stopwatch('B')
]);
const [selectedStopwatchIndex, setSelectedStopwatchIndex] = useState(-1);
let selectedStopwatch = selectedStopwatchIndex == -1
? null
: stopwatches[selectedStopwatchIndex];
return (
<>
<StopwatchList
stopwatches={stopwatches}
setStopwatches={setStopwatches}
selectedStopwatchIndex={selectedStopwatchIndex}
setSelectedStopwatchIndex={setSelectedStopwatchIndex}
/>
<StopwatchDetails
stopwatch={selectedStopwatch}
removeStopwatch={() => {
// Проблемная функция
}}
/>
</>
);
}
function StopwatchDetails({ stopwatch, removeStopwatch }) {
const [time, setTime] = useState('0:00:00');
useEffect(() => {
if (!stopwatch) return;
const interval = setInterval(() => {
setTime(stopwatch.getElapsedTimeFormatted());
}, 100);
return () => {
clearInterval(interval);
}
}, [stopwatch]);
return (
<>
<p>
{stopwatch == null
? 'Информация о выбранном секундомере будет отображаться здесь!'
: `Время: ${time}`}
</p>
<button onClick={removeStopwatch}>Удалить</button>
</>
);
}
Я также создал CodeSandbox, демонстрирующий эту проблему: https://codesandbox.io/p/sandbox/recursing-napier-ly4td9
Подход с фильтром не работает из-за проблемы замыкания, где функция removeStopwatch захватывает устаревшее значение selectedStopwatchIndex, в то время как подход с splice работает, потому что он relies на текущее значение состояния внутри функции обновления.
Содержание
- Понимание проблемы замыкания
- Почему Splice Работает
- Почему Filter Не Работает
- Решения для исправления проблемы
- Лучшие практики для обновления состояния в React
Понимание проблемы замыкания
Когда вы передаете функцию removeStopwatch из MainScreen в StopwatchDetails, она захватывает текущее значение selectedStopwatchIndex в своем замыкании. Вот что происходит:
// В компоненте MainScreen
<StopwatchDetails
stopwatch={selectedStopwatch}
removeStopwatch={() => {
// Эта функция захватывает selectedStopwatchIndex в момент создания
setStopwatches(s => {
const newArray = [...s];
newArray.splice(selectedStopwatchIndex, 1); // Использует устаревшее значение
return newArray;
});
}}
/>
Проблема в том, что selectedStopwatchIndex может измениться (стать -1 или другим значением) между моментом создания функции и моментом ее фактического вызова по нажатию кнопки в StopwatchDetails.
Почему Splice Работает
Подход с splice кажется работающим, потому что он использует функциональную форму обновления setStopwatches:
setStopwatches(s => {
// 's' - это текущее значение состояния
const newArray = [...s];
newArray.splice(selectedStopwatchIndex, 1); // Все еще использует устаревшее значение
return newArray;
});
Однако это вводит в заблуждение. Подход с splice работает только потому, что selectedStopwatchIndex к моменту выполнения функции все еще оказывается правильным, или потому что ошибка менее заметна.
Почему Filter Не Работает
Подход с filter не работает по той же причине - он использует захваченное устаревшее значение:
removeStopwatch={() => {
setStopwatches(s => {
const newArray = [...s];
return newArray.filter((_, i) => i !== selectedStopwatchIndex); // Устаревшее значение
});
}}
Разница более заметна с filter, потому что:
- Filter обрабатывает весь массив
- Если
selectedStopwatchIndexравен -1 (нет выбора), filter ничего не удаляет - Если
selectedStopwatchIndexустарел, он удаляет неверный элемент
Решения для исправления проблемы
Решение 1: Передавать индекс напрямую в обработчик
function MainScreen() {
// ... существующее состояние
const handleRemoveStopwatch = (indexToRemove) => {
setStopwatches(s => {
return s.filter((_, i) => i !== indexToRemove);
});
};
return (
<>
<StopwatchList
stopwatches={stopwatches}
setStopwatches={setStopwatches}
selectedStopwatchIndex={selectedStopwatchIndex}
setSelectedStopwatchIndex={setSelectedStopwatchIndex}
/>
<StopwatchDetails
stopwatch={selectedStopwatch}
removeStopwatch={() => handleRemoveStopwatch(selectedStopwatchIndex)}
/>
</>
);
}
Решение 2: Использовать useCallback с зависимостями
function MainScreen() {
// ... существующее состояние
const handleRemoveStopwatch = useCallback(() => {
if (selectedStopwatchIndex === -1) return;
setStopwatches(s => s.filter((_, i) => i !== selectedStopwatchIndex));
}, [selectedStopwatchIndex]);
return (
<>
<StopwatchList
stopwatches={stopwatches}
setStopwatches={setStopwatches}
selectedStopwatchIndex={selectedStopwatchIndex}
setSelectedStopwatchIndex={setSelectedStopwatchIndex}
/>
<StopwatchDetails
stopwatch={selectedStopwatch}
removeStopwatch={handleRemoveStopwatch}
/>
</>
);
}
Решение 3: Включить индекс в StopwatchDetails
function StopwatchDetails({ stopwatch, index, removeStopwatch }) {
// ... существующая логика
return (
<>
<p>
{stopwatch == null
? 'Информация о выбранном секундомере будет отображаться здесь!'
: `Время: ${time}`}
</p>
<button onClick={() => removeStopwatch(index)}>Удалить</button>
</>
);
}
// В MainScreen:
<StopwatchDetails
stopwatch={selectedStopwatch}
index={selectedStopwatchIndex}
removeStopwatch={(index) => setStopwatches(s => s.filter((_, i) => i !== index))}
/>
Лучшие практики для обновления состояния в React
1. Избегайте замыканий
Никогда не передавайте функции, которые захватывают значения состояния, напрямую дочерним компонентам. Вместо этого:
- Передавайте значение в качестве параметра
- Используйте
useCallbackс правильными зависимостями - Создавайте обработчик в дочернем компоненте
2. Предпочитайте неизменяемость
И splice, и filter могут использоваться неизменяемо, но filter обычно предпочтительнее из-за своей ясности:
// Хорошо - неизменяемый filter
setStopwatches(prev => prev.filter((_, i) => i !== index));
// Хорошо - неизменяемый spread + splice (более многословно)
setStopwatches(prev => {
const newArray = [...prev];
newArray.splice(index, 1);
return newArray;
});
3. Учитывайте производительность для больших массивов
Как отмечено в исследованиях, для больших массивов оба подхода имеют сложность O(n), но filter обрабатывает весь массив, в то время как splice обрабатывает только оставшиеся элементы после точки удаления.
4. Используйте Immer для сложных обновлений
Для сложных обновлений состояния рассмотрите возможность использования Immer:
import { produce } from 'immer';
setStopwatches(prev =>
produce(prev, draft => {
draft.splice(selectedStopwatchIndex, 1);
})
);
Заключение
Основная проблема - это проблема замыкания, когда функция removeStopwatch захватывает устаревшее значение selectedStopwatchIndex. Оба подхода с splice и filter работали бы правильно, если бы они использовали текущее значение состояния, но замыкание захватывает значение в момент создания функции.
Ключевые выводы:
- Всегда передавайте текущие значения состояния в качестве параметров обработчикам
- Используйте
useCallbackс правильными зависимостями при передаче обработчиков - Filter обычно предпочтительнее из-за своей ясности и неизменяемости
- Будьте внимательны к проблемам замыкания при передаче функций между компонентами
Наиболее надежное решение - передавать индекс напрямую в функцию обработчика или использовать useCallback с правильными зависимостями для обеспечения работы с текущими значениями состояния.
Источники
- React Dev - Обновление массивов в состоянии
- Stack Overflow - Использование splice для обновления состояния в React
- Stack Overflow - В чем преимущество использования $splice вместо filter
- Ultimate Courses - Неизменяемые массивы в JavaScript
- GitHub - Immutability Helper
- Redux - Шаблоны неизменяемых обновлений