Как предотвратить срабатывание useCallback при использовании с useEffect (и соответствовать требованиям eslint-plugin-react-hooks)?

У меня есть вариант использования, когда страница должна вызывать одну и ту же функцию выборки при первом рендеринге и при нажатии кнопки.

Код аналогичен приведенному ниже (ссылка: https://stackblitz.com/edit/stackru-question-bink?file=index.tsx):

import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { fetchBackend } from './fetchBackend';

const App: FunctionComponent = () => {
  const [selected, setSelected] = useState<string>('a');
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<boolean>(false);
  const [data, setData] = useState<string | undefined>(undefined);

  const query = useCallback(async () => {
    setLoading(true)

    try {
      const res = await fetchBackend(selected);
      setData(res);
      setError(false);
    } catch (e) {
      setError(true);
    } finally {
      setLoading(false);
    }
  }, [])

  useEffect(() => {
    query();
  }, [query])

  return (
    <div>
      <select onChange={e => setSelected(e.target.value)} value={selected}>
        <option value="a">a</option>
        <option value="b">b</option>
      </select>
      <div>
        <button onClick={query}>Query</button>
      </div>
      <br />
      {loading ? <div>Loading</div> : <div>{data}</div>}
      {error && <div>Error</div>}
    </div>
  )
}

export default App;

Проблема для меня в том, что функция выборки всегда запускается при любом изменении ввода, потому что eslint-plugin-react-hooks заставляет меня объявить все зависимости (например, выбранное состояние) в useCallbackкрючок. И я должен использоватьuseCallback чтобы использовать его с useEffect.

Я знаю, что могу поместить функцию вне компонента и передать все аргументы (props, setLoading, setError,... и т. Д.), Чтобы это работало, но мне интересно, можно ли заархивировать тот же эффект, сохраняя функция выборки внутри компонента и соответствует eslint-plugin-react-hooks?

3 ответа

Решение

Добавьте всех своих иждивенцев в useCallback как обычно, но не заставляйте использовать другую функцию Эффект:

useEffect(query, [])

Для асинхронных обратных вызовов (например, запроса в вашем случае) вам нужно будет использовать обещание в старом стиле с.then, .catch а также .finally обратные вызовы, чтобы передать функцию void в useCallback, что требуется useEffect.

Другой подход можно найти в документации React, но согласно документам он не рекомендуется.

В конце концов, встроенные функции передаются в useEffectв любом случае повторно объявляются при каждом повторном рендеринге. При первом подходе вы передадите новую функцию только тогда, когда изменится глубина запроса. Предупреждения тоже должны исчезнуть.;)

Есть несколько моделей для достижения чего-то, где вам нужно вызывать функцию выборки при монтировании компонента и при нажатии кнопки / другого. Здесь я предлагаю вам другую модель, в которой вы можете добиться того, что вы используете только хуки и не вызываете функцию выборки напрямую на основе нажатия кнопки. Это также поможет вам удовлетворить правила eslint для массива hook deps и легко избежать бесконечного цикла. Фактически, это будет использовать силу эффекта, называемогоuseEffect и другое существо useState. Но если у вас есть несколько функций для получения разных данных, вы можете рассмотреть множество вариантов, например подход useReducer. Что ж, посмотрите на этот проект, где я пытался добиться чего-то похожего на то, что вы хотели.

https://codesandbox.io/s/fetch-data-in-react-hooks-23q1k?file=/src/App.js

Поговорим немного о модели

export default function App() {
  const [data, setDate] = React.useState("");
  const [id, setId] = React.useState(1);
  const [url, setUrl] = React.useState(
    `https://jsonplaceholder.typicode.com/todos/${id}`
  );
  const [isLoading, setIsLoading] = React.useState(false);

  React.useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(json => {
        setDate(json);
        setIsLoading(false);
      });
  }, [url]);

  return (
    <div className="App">
      <h1>Fetch data from API in React Hooks</h1>
      <input value={id} type="number" onChange={e => setId(e.target.value)} />
      <button
        onClick={() => {
          setIsLoading(true);
          setUrl(`https://jsonplaceholder.typicode.com/todos/${id}`);
        }}
      >
        GO & FETCH
      </button>
      {isLoading ? (
        <p>Loading</p>
      ) : (
        <pre>
          <code>{JSON.stringify(data, null, 2)}</code>
        </pre>
      )}
    </div>
  );
}

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

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

useEffect(() => {
    query();
  }, [data]) // Only re-run the effect if data changes

Для получения подробной информации перейдите в конец этой официальной страницы документации.

Другие вопросы по тегам