Перехватчики React: зачем useEffect исчерпывающий массив зависимостей? - PullRequest
5 голосов
/ 02 августа 2020

У меня есть следующий вариант использования компонента React.

Это пользовательский ввод для поиска, который использует React Autosuggest . Его значение всегда является идентификатором, поэтому я использую только идентификатор пользователя в качестве опоры. Поэтому при первой загрузке, чтобы показать значение имени пользователя, мне нужно получить его при первом монтировании.

РЕДАКТИРОВАТЬ: я не хочу получать значение снова, когда оно изменится позже, потому что у меня уже есть значение из моего запрос предложений.

type InputUserProps = {
  userID?: string;
  onChange: (userID: string) => void;
};

// Input User is a controlled input
const InputUser: React.FC<InputUserProps> = (props) => {
  const [username, setUsername] = useState<string | null>(null);

  useEffect(() => {
    if (props.userID && !username) {
      fetchUsername(props.userID).then((username) => setUsername(username));
    }
  }, []);

  async function loadSuggestions(inputValue: string): Suggestion[] {
    return fetchSuggestions(inputValue);
  }

  function selectSuggestion(suggestion: Suggestion): void {
    props.onChange(suggestion.userID); // I don't want to rerun the effect from that change...
    setUsername(suggestion.username); // ...because I set the username here
  }

  return (
    <InputSuggest
      value={props.userID}
      label={username}
      onSuggestionsRequested={loadSuggestions}
      onSuggestionSelected={selectSuggestion}
    />
  );
};

export default InputUser;

(я добавляю упрощенный способ вызова этого компонента)

const App: React.FC<AppProps> = (props) => {
    // simplified, I use react-hook-form in the real code
    const [userID, setUserID] = useState<string?>(any_initial_value_or_null);
    return <InputUser userID={userID} onChange={(newValue)=>setUserID(newValue)} />
};

export default App;

Он работает, но у меня есть следующее предупреждение о моем хуке useEffect

React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)

Но если я это сделаю, я запущу его более одного раза, так как значение имени пользователя изменяется самим хуком! Поскольку он работает без всех зависимостей, мне интересно:

  • Как я могу решить мой случай чисто?
  • Для чего эти зависимости и почему рекомендуется использовать исчерпывающий их?

Ответы [ 3 ]

1 голос
/ 02 августа 2020

Похоже, что userId действительно должна быть зависимостью, поскольку, если она изменится, вы захотите снова запустить свой запрос.

Я думаю, вы можете отказаться от проверки для username и всегда получать, когда, и только при изменении userId:

useEffect(() => {
    if(props.userID) {
        fetchUsername(props.userID)
            .then((username) => setUsername(username))
    }
}, [props.userID])

В общем, вы хотите перечислить все закрывающие переменные в своем эффекте, чтобы избежать устаревших ссылок при выполнении эффекта.

- ИЗМЕНИТЬ, чтобы адресные вопросы OP: поскольку в вашем случае использования вы знаете, что хотите выполнить действие только при начальном монтировании, передача пустого массива зависимостей является допустимым подходом.

Другой вариант - отслеживать полученный идентификатор пользователя, например

const fetchedIds = useRef(new Set())

всякий раз, когда вы получаете имя пользователя для нового идентификатора, вы можете обновить ссылку:

fetchedIds.current.add(newId)

и в своем эффекте вы можете протестировать:

if (props.userID && !fetchedIds.current.has(props.userID)) {
   // do the fetch
}
1 голос
/ 02 августа 2020

Что это за зависимости?

useEffect принимает необязательный второй аргумент, который представляет собой массив зависимостей. Зависимости хука useEffect говорят ему запускать эффект всякий раз, когда его зависимость изменяется.

Если вы не передадите необязательный второй аргумент для хука useEffect, он будет выполняться каждый раз, когда компонент повторно визуализирует . Пустой массив зависимостей указывает, что вы хотите запустить эффект только один раз после первоначального рендеринга компонента. В этом случае ловушка useEffect будет вести себя почти как componentDidMount в компонентах класса.

почему рекомендуется делать их исчерпывающими?

Эффекты см. props и state из рендеринга, в котором они были определены. Поэтому, когда вы используете что-то из области функционального компонента, который участвует в потоке данных реакции, например реквизиты или состояние, внутри функции обратного вызова хука useEffect, эта функция функции обратного вызова закрывается над этими данными, и если новый эффект не определен с новыми значениями props и state, ваш эффект будет видеть устаревшие props и значения состояния.

Следующий фрагмент кода демонстрирует, что может go неправильно, если вы l ie о зависимостях хука useEffect.

В следующем фрагменте кода есть два компонента: App и User. Компонент App имеет три кнопки и поддерживает идентификатор пользователя, который отображается компонентом User. Идентификатор пользователя передается как опора из App в User компонент, а User компонент выбирает пользователя с идентификатором, переданным как опора, из jsonplaceholder API.

Теперь проблема в следующий фрагмент кода говорит о том, что он работает неправильно. Причина в том, что это ложь о зависимости хука useEffect. useEffect hook зависит от userID prop для получения пользователя из API, но поскольку я пропустил добавление userID в качестве зависимости, useEffect hook не выполняется каждый раз, когда userID prop изменяется.

function User({userID}) {
  const [user, setUser] = React.useState(null);
  
  React.useEffect(() => {
    if (userID > 0) {
      fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
        .then(response => response.json())
        .then(user => setUser(user))
        .catch(error => console.log(error.message));
    }
  }, []); 
  
  return (
    <div>
      {user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
    </div>
  );
}

function App() {
  const [userID, setUserID] = React.useState(0);
  
  const handleClick = (id) => {
    setUserID(id);
  };
  
  return(
    <div>
      <button onClick={() => handleClick(1)}>User with ID: 1</button>
      <button onClick={() => handleClick(2)}>User with ID: 2</button>
      <button onClick={() => handleClick(3)}>User with ID: 3</button>
      <p>Current User ID: {userID}</p>
      <hr/>
      <User userID={userID} />
    </div>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Приведенный выше фрагмент кода показывает одну из немногих проблем, которые могут возникнуть, если вы ie о зависимостях, и вот почему вы не должны пропускать или l ie о зависимостях ловушки useEffect или любой другой ловушки, которая имеет массив зависимостей, например useCallback hook. '

Чтобы исправить предыдущий фрагмент кода , вам просто нужно добавить userID как зависимость к массиву зависимостей хука useEffect, чтобы if выполнялся всякий раз, когда userID prop изменяется, и новый пользователь выбирается с идентификатором, равным userID prop.

function User({userID}) {
  const [user, setUser] = React.useState(null);
  
  React.useEffect(() => {
    if (userID > 0) {
      fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
        .then(response => response.json())
        .then(user => setUser(user))
        .catch(error => console.log(error.message));
    }
  }, [userID]); 
  
  return (
    <div>
      {user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
    </div>
  );
}

function App() {
  const [userID, setUserID] = React.useState(0);
  
  const handleClick = (id) => {
    setUserID(id);
  };
  
  return(
    <div>
      <button onClick={() => handleClick(1)}>User with ID: 1</button>
      <button onClick={() => handleClick(2)}>User with ID: 2</button>
      <button onClick={() => handleClick(3)}>User with ID: 3</button>
      <p>Current User ID: {userID}</p>
      <hr/>
      <User userID={userID} />
    </div>
  );
}

ReactDOM.render(<App/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>

В вашем случае, если вы пропустите добавление props.userID в массив зависимостей хука useEffect, ваш эффект не будет получать новые данные, когда опора userID изменений.

Чтобы узнать больше о негативных последствиях исключения зависимостей хука useEffect, см .:

Как я могу аккуратно решить мой случай?

Поскольку ваш эффект зависит от значения prop userID, вы должны включить он в массиве зависимостей, чтобы всегда получать новые данные при изменении userID.

Добавление props.userID в качестве зависимости к useEffect хук будет вызывать эффект каждый раз, когда props.userID изменяется, но проблема в что вы без необходимости используете username внутри useEffect. Вы должны удалить его, потому что в этом нет необходимости, поскольку значение username не определяет и не должно определять, когда должны быть получены новые данные пользователя. Вы просто хотите, чтобы эффект запускался всякий раз, когда изменяется props.userID.

Вы также можете отделить действие от обновления состояния, используя ловушку useReducer для управления и обновления состояния.

Редактировать

Поскольку вы хотите запустить эффект только тогда, когда userID используется ловушкой useEffect, в вашем случае можно иметь пустой массив в качестве второго аргумента и игнорировать предупреждение eslint. Вы также можете не опускать какие-либо зависимости хука useEffect и использовать какое-то условие в useEffect хуке, которое оценивается как false после первого запуска эффекта и обновляет состояние.

Лично я бы посоветовал вам чтобы попытаться изменить структуру ваших компонентов, чтобы вам не приходилось сталкиваться с подобными проблемами в первую очередь.

0 голосов
/ 02 августа 2020

Если вы уверены, что перед монтированием компонента InputUser все зависимые реквизиты были заполнены и имеют правильное значение, тогда добавьте eslint-disable-next-line react-hooks/exhaustive-deps прямо перед строкой }, []), но если зависимые реквизиты не имеют значения при первом монтировании компонента , Поэтому вам нужно добавить их в зависимости useEffect.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...