Легко упустить детали в управлении состоянием при использовании API в функциональных компонентах React и некоторые шаблоны для решения возникших проблем

У нас сложная кодовая база реагирования, и мы в конечном итоге используем множество различных шаблонов реагирования из-за базовой сложности продукта и данных. Недавно мы заметили небольшую ошибку при использовании вызова api в родительском элементе и передаче состояния вызова api дочерним элементам. Этот пост основан на некоторых уроках, извлеченных из этой ошибки, мы обсудим вещи, которые легко упустить при обработке состояния api из функциональных компонентов React.

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

Рассмотрим этот компонент:

// Approach1
function ComponentWithApi() {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [data, setData] = useState();
  useEffect(() => {
    fetchData()
     .then((res) => {
       // try to change the order of the setData and setLoading here
       // and see if the UI breaks
       setData(res.data);
       setLoading(false);
     })
     .catch((err) => {
       setLoading(false);
       setError(true);
       setData();
     });
  }, []);
  return loading ? (
      "Loading..."
    ) : error ? (
      "Error!"
    ) : (
      <div> Approach1 value: {data.property} </div>
    );
}

Компонент выполняет вызов api в useEffect и соответственно отображает состояние загрузки / ошибки / данных.

Поначалу такой простой подход выглядит неплохо:

  • мы начинаем с загрузки как true
  • в рендере мы охватили все состояния
    - если загрузка - показать текст загрузки
    - в противном случае, если ошибка - показать текст ошибки
    - в противном случае, если не загрузка и не ошибка, данные должны быть доступны, так покажи данные

Это работает нормально, но есть некоторые тонкие ошибки в коде и проблемы с пониманием в приведенной выше настройке:

  • Мне лично было бы лучше, если бы состояние загрузки было совмещено с вызовом api, а не было установлено как true по умолчанию
  • Возможно, у вас уже есть понимание, что setState является асинхронным + пакетным в React, но если вы измените порядок setLoading и setData в приведенном выше коде, вы увидите, что код ломается, и что понимание здесь неверно.
  • как только код сломается и вы поймете причину (для этого потребуется целая отдельная статья, но TL; DR; в текущей реализации setState вызовы являются только асинхронными внутри обработчиков событий, но не внутри асинхронных обратных вызовов - обещаний / таймаутов), вы будете также осознайте, что на самом деле существует возможное переходное состояние, когда loading: false, data: undefined, error: false
  • Мне лично хотелось бы, чтобы эти связанные состояния можно было каким-то образом обрабатывать вместе, вместо того, чтобы иметь useState для каждого из них по отдельности.

Давайте посмотрим на решение этих проблем по очереди:

  • Чтобы setState не был асинхронным внутри обещания, что приводит к переходному состоянию loading: false, data: undefined, error: false, мы можем сделать одно из двух:
    - убедиться, что порядок setState правильный внутри обещания. Затем (мы увидим лучшая обработка для этого в более позднем шаблоне)
    - или обработать состояние для данных, которые не являются недоступными явно. Это также помогает изначально установить для состояния загрузки значение false, поскольку оно совпадает с этим переходным состоянием (что является одной из наших проблем, как обсуждалось выше).
// Approach 2
function ComponentWithApi() {
  // setting the initial loading state to false
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);
  const [data, setData] = useState();
  
  useEffect(() => {
    fetchData()
      .then((res) => {
        setData(res.data);
        setLoading(false);
      })
      .catch((err) => {
        setLoading(false);
        setError(true);
        setData();
      });
  }, []);
  return loading ? (
    "Loading..."
  ) : error ? (
    "Error!"
  ) : data ? (
    <div> Approach2 value: {data.property} </div>
  ) : null;
}

С помощью описанного выше подхода мы решили следующие проблемы:

  • состояние загрузки совпадает с вызовом API и изначально установлено в false
  • мы решили неизвестное состояние, явным образом обработав его при рендеринге и вне зависимости от того, как реагировать на асинхронное и синхронное состояние реакции.

Нам все еще необходимо решить указанную ниже проблему / неудобство:

  • вместо того, чтобы иметь отдельный useState для каждого из этих подключенных состояний, имело бы смысл обновить их все сразу. Это решит нашу проблему временного неизвестного состояния по сути, потому что это временное состояние не произойдет, когда мы обновим все связанные состояния сразу (хотя подобное состояние все равно будет происходить при первоначальном рендеринге, поэтому нам все равно придется явно обрабатывать данные, которые в любом случае не определены. )
// Approach 3
const initialState = {
  loading: false,
  error: false,
  data: null
};
const dataReducer = (state, action) => {
  switch (action.type) {
   case "LOADING":
    return { ...state, loading: true, error: false, data: null };
   case "LOADED":
    return { ...state, loading: false, error: false, data: action.payload };
   case "ERROR":
    return { ...state, loading: false, error: true, data: null };
   default:
    return state;
  }
};
function ComponentWithApi() {
  const [api, dispatch] = useReducer(dataReducer, undefined, dataReducer);
  
  useEffect(() => {
    dispatch({ type: "LOADING " });
    fetchData()
      .then((res) => {
        dispatch({ type: "LOADED", payload: res.data });
      })
      .catch((err) => {
        dispatch({ type: "ERROR" });
      });
  }, []);
return api.loading ? (
    "Loading..."
  ) : api.error ? (
    "Some error occurred"
  ) : api.data ? (
    <div>Approach3 Value: {api.data.property}</div>
  ) : null;
}

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

  • позволяет обновлять все связанные состояния сразу
  • позволяет потенциально извлекать настраиваемый хук для простой выборки данных
  • кажется более декларативным, вместо того, чтобы организовывать отдельные части связанного состояния
  • Если в компоненте есть несколько вызовов api, отдельные состояния вызовов api могут быть инкапсулированы в их собственные редукторы, что делает код более читаемым.

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

Использованная литература: