Скажем, у меня есть родительский компонент, который отображает дочерний компонент, и все, что делает дочерний компонент, — это сохраняет некоторое состояние и запускает обработчик onChange, если его собственное состояние изменяется. См. приведенный ниже код или CodeSandbox.
Этот код попадает в бесконечный цикл, и я хочу избавиться от этого поведения.
Возможные решения, но те, которые я не предпочитаю:
1: Я мог бы поместить все состояние в родительский компонент и управлять дочерним компонентом с помощью родителя, но это не совсем то, чего я хочу. В реальной жизни мой дочерний компонент заботится не только о простом счетчике, и я хочу легко использовать его из родительского компонента. Внутри дочернего компонента происходит сложное поведение, и я хочу сообщить о некоторых простых изменениях родительскому компоненту. Или это абсолютно запрещено в React? (чтобы сохранить состояние в дочернем, а также инициировать изменение состояния при обновлении родительского) Я бы сказал, что это не обязательно?
2: я также мог бы вызвать обработчик onChange из обработчика handleInternalChange. К тому же не то, что я хочу. В реальной жизни мой дочерний компонент будет обновляться из нескольких отдельных мест в самом компоненте, поэтому изменение состояния — это самая элегантная вещь, которую нужно наблюдать и запускать родитель onChange.
3: я мог бы опустить зависимость onChange в массиве зависимостей useEffect. Это не рекомендуется, сообщество React ссылается на это объяснение. Как я понимаю, только я чувствую, что эта ситуация будет исключением?** Кроме того, я использую CRA, который поставляется с отличными линтерами и т. д. из коробки, и линтер жалуется на это, если я удаляю обработчик onChange из зависимостей, и я не предпочитаю начинать варить свои собственные правила лайнера. Для такого простого варианта использования, как мой, линтеры, установленные сообществом, должны работать нормально.
Что я думаю, что происходит Я думаю, что происходит то, что родитель обновляется, затем перерисовывает все это, и каким-то образом обработчик onChange также «меняется». Функция на самом деле не меняется, насколько я бы сказал, но React думает (или знает), что это так, поэтому он снова запускает вызов useEffect в дочернем компоненте, а затем рождается бесконечный цикл.
Но насколько я понимаю, функция onChange не меняется. Так почему же срабатывает вызов useEffect? И как я могу предотвратить это?
import React, { useState, useEffect } from "react";
const Comp = ({ onChange }) => {
const [internalState, setInternalState] = useState(0);
const handleChangeEvent = () => {
setInternalState(internalState + 1);
};
useEffect(() => {
const result = internalState.toString();
onChange(result);
}, [internalState, onChange]);
return (
<div onClick = {handleChangeEvent}>
CLICK ME
<div>{`INTERNAL NUM: ${internalState}`}</div>
</div>
);
};
export default function App() {
const [state, setState] = useState("");
const handleChangeEvent = () => setState(state + 1);
return (
<div className = "App">
<h3>{`STATE: ${state}`}</h3>
<Comp onChange = {handleChangeEvent} />
</div>
);
}
** Через некоторое время, в других случаях, свойство onChange, конечно, может быть изменено, просто назначив ему другую функцию. Так что это правило мне совершенно ясно. Только (как сказано в последнем абзаце), почему он ведет себя так, как будто он меняется в этом сценарии? Так как моя функция совершенно не меняется.
Since my function does not change at all.
Он меняется каждый раз, когда вы визуализируете родителя.
Ваши реквизиты onChange
постоянно меняются, потому что каждый раз, когда родительский компонент изменяется, он повторно создает экземпляр handleChangeEvent
, который никогда не будет таким же, как предыдущий. Я думаю, вам следует выбрать решение 1: сохранить состояние в родительском компоненте и передать его в качестве опоры дочернему компоненту; это никоим образом не должно ограничивать вас, так как ваш дочерний компонент все еще может иметь свое собственное локальное состояние и выполнять все сложные операции, которые он должен выполнять.
Возможно, вы захотите проверить хук useCallback
Вы должны обернуть метод handleChangeEvent хуками useCallback, чтобы он был создан один раз.
const handleChangeEvent = useCallback(() => setState(state + 1),[]);
Бесконечный цикл возникает из-за того, что вы добавили метод onChange
в качестве зависимости для useEffect в компоненте <Comp />
.
useEffect принимает массив зависимостей и запускает эффект при изменении одной из зависимостей.
Поскольку вы добавили обработчик onchange в качестве зависимости, каждый раз при повторном рендеринге родительского компонента создается новый экземпляр метода handleChangeEvent
, который не равен предыдущему методу handlechange.
Процесс рендеринга компонента будет таким:
<App />
компонент создает handleChangeEvent
метод и передает его <Comp />
<Comp />
будет вызываться после первоначального рендеринга, а оттуда будет вызываться метод handleChangeEvent компонента <App />
.<App />
и повторно отображает его. При повторном рендеринге создается новый экземпляр handleChangeEvent и передается свойство onChange компоненту <Comp />
.Чтобы предотвратить это, метод handleChangeEvent должен быть обернут с помощью useCallback. Функция обратного вызова, переданная в хук useCallback, будет запомнена, поэтому, когда дочерний компонент сравнивает старую и новую опору, они остаются равными.
{} === {} // false
[] === [] // false
(() => {}) == (() => {}) //false
Ваш
useEffect
действительно не имеет здесь смысла,. Вы получаете бесконечный цикл, потому что вашuseEffect
вызывается каждый раз, и этот эффект вызывает обратный вызовonChange
, поэтому бесконечный цикл.