Ответ 1
Это общая проблема для функциональных компонентов, которые используют хук useState
. То же самое относится к любым функциям обратного вызова, где используется состояние 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
, ссылки не применяются.
Можно сохранять обновления состояния и изменяемое состояние отдельно, но forceUpdate
считается антипаттерном как для компонентов класса, так и для компонентов функций (перечислены только для справки):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
Функция обновления состояния
Одним из решений является использование функции обновления состояния, которая получает свежее состояние вместо устаревшего состояния из внешней области видимости:
function eventListener() {
// does not 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
в версии React 16.7.0-alpha, но не работоспособно в окончательной реализации React 16.8. useState
в настоящее время поддерживает только неизменяемое состояние.