Это общая проблема для функциональных компонентов, которые используют useState
hook.Те же проблемы применимы к любым функциям обратного вызова, где используется состояние useState
, например, setTimeout
или setInterval
функции таймера .
Обработчики событий обрабатываются по-разному в CardsProvider
иCard
компоненты.
handleCardClick
и handleButtonClick
, используемые в CardsProvider
функциональном компоненте, определены в его области действия.Каждый раз, когда он запускается, появляются новые функции, они ссылаются на состояние cards
, которое было получено в момент их определения.Обработчики событий перерегистрируются каждый раз при визуализации компонента CardsProvider
.
handleCardClick
, используемый в Card
функциональном компоненте, принимается как реквизит и регистрируется один раз при монтировании компонента с помощью useEffect
.Это та же самая функция в течение всего срока службы компонента, и она относится к устаревшему состоянию, которое было свежим в то время, когда функция handleCardClick
была определена впервые.handleButtonClick
принимается как реквизит и перерегистрируется при каждом рендере Card
, это новая функция каждый раз и относится к свежему состоянию.
Изменяемое состояние
Общий подход, который обращается кэта проблема заключается в использовании useRef
вместо useState
.Ссылка - это, по сути, рецепт, который предоставляет изменяемый объект, который может быть передан по ссылке:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
В случае, если компонент должен быть повторно визуализирован при обновлении состояния, как это ожидается от useState
, refs arenНеприменимо.
Можно хранить обновления состояния и изменяемое состояние отдельно, но forceUpdate
считается антипаттерном как для компонентов класса, так и для компонентов функций (перечислены только для справки):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
Функция обновления состояния
Одним из решений является использование функции обновления состояния, которая получает свежее состояние вместо устаревшего состояния из охватывающей области действия:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
В случае, если требуется состояние для синхронного побочного эффекта, такого как console.log
, обходной путь - вернуть то же состояние, чтобы предотвратить обновление.
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
}, []);
Это не очень хорошо работает с асинхронными побочными эффектами, в частности async
функциями.
Ручная перерегистрация прослушивателя событий
Другим решением является повторная регистрациярегистрируйте прослушиватель событий каждый раз, поэтому обратный вызов всегда получает новое состояние из окружающей области:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
}, [state]);
Встроенная обработка событий
Если прослушиватель событий не зарегистрирован в document
, window
или другие цели событий находятся вне области действия текущего компонента, при необходимости следует использовать собственную обработку событий DOM в React, что устраняет необходимость в useEffect
:
<button onClick={eventListener} />
В последнем случае слушатель событий можетдополнительно запомните с помощью useMemo
или useCallback
, чтобы предотвратить ненужные повторные рендеринг, когда он передается как реквизит:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
В предыдущей редакции ответа предлагалось использовать изменяемое состояние, применимое кначальная реализация useState
hook в версии React 16.7.0-alpha, но не работоспособна в финальной реализации React 16.8.useState
в настоящее время поддерживает только неизменяемое состояние.