React. Fetch и useEffect. Рендер компонента несколько раз

Рейтинг: 0Ответов: 1Опубликовано: 06.04.2023
export const API = (url, exportedData, isLoading) => {
    fetch(url, {
        method: "GET",
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${token}`,
        },
    })
        .then((response) => {
            if (response.ok) {
                return response.json();
            } else {
                return response.json().then((data) => {
                    let errorMess = "Auth failed";
                    if (data && data.errors) {
                        errorMess = data.errors;
                    }
                    console.log(errorMess);
                    throw new Error(errorMess);
                });
            }
        })
        .then((data) => {
            exportedData(data.payload);
            isLoading(false);
        })
        .catch((err) => {
            console.log(err);
        });
};


import { Fragment, useState, useEffect } from "react";
import { API } from "../inc/helpers";
import Loading from "../components/Loading";

const Profile = () => {
    console.log("render Profile"); //срабатывает 3 раза

    const [fetchedData, setFetchedData] = useState({});
    const [isLoading, setIsLoading] = useState(true);

    const getData = (data) => setFetchedData({ ...data });

    useEffect(() => {
        API(
            "LINK/profile",
            getData,
            setIsLoading
        );
        console.log("useEffect Profile"); //срабатывает 2 раза
    }, [isLoading]);

Что я делаю не так, что он рендерит несколько раз одно и тоже?

Ответы

▲ 1Принят

Постараюсь это объяснить в два этапа, во втором со снипетом, предпочтительнее создавать именно снипеты, тем более сайт это позволяет.

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

Первое, что видится, это то что Вы дергаете setState из другого места (не из Компонента), это не очень здорово может порождать проблемы в некоторых случаях. Что касается catch, то он должен обрабатыватся тоже в Компоненте, иначе вы не получите return в некоторых случаях — лучше все вернуть в useEffect и далее с этим работать. catch должен быть там где вы будете его как-то обрабатывать, например Вы можете захотеть отображать в компоненте, что загрузка не удалась. А console.log тоже не очень здорово писать вне методов Компонента (т.е. она отражает не то что Вы хотите, первый рендер он обязателен после монтирования, а далее Вы 2 раза обновляете стейт*, обновляя данные, и обновляя статус загрузки, поэтому всего их 3).

const API = (url) => {
  return fetch(url, {
    method: "GET",
    headers: { "Content-Type": "application/json" },
  }).then((response) => response.json()) // это можно оставить тут
};

const Profile = () => {
    console.log("render Profile", Date.now()); //срабатывает 3 раза
    const [fetchedData, setFetchedData] = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    useEffect(() => {
      (async () => {
          if (!isLoading){
            API("https://jsonplaceholder.typicode.com/todos11/1")
              .then(data => {
                  setFetchedData(data);
                  setIsLoading(true) // <-- вызывает дополниельный рендер
              })
              .catch((err) => {
                console.log(`Что-то пошло не так: ${err}`);
              });
            console.log("useEffect Profile: fetching"); //срабатывает 1 раз
          } else {
            console.log("useEffect Profile"); //срабатывает 1 раз
          }
      })();
    }, [isLoading]);

    return <pre>Какая-то верстка {
      isLoading 
        ? JSON.stringify(fetchedData, null, 2)
        : 'Loading...'
    }</pre>
}

По итогу у нас такая картина:

  • render Profile (монтирование и первый рендер )
  • useEffect Profile: (причина: isLoading==false, загрузка данных)
  • render Profile (причина: *обновление state fetchedData)
  • render Profile (причина: *обновление state isLoading==true)
  • useEffect (причина: обновление зависимости isLoading==true)

введите сюда описание изображения

Эта картинка жизненного цикла показывает когда происходит рендер — после монтирования или обновления состояния (state) или свойств (props).

Способ избежать дополнительного рендера

Сохранить удобство написания кода, можно используя хук useReducer:

<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin ></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin ></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
  window.useState = React.useState;
  window.useEffect = React.useEffect
  window.useReducer = React.useReducer 

  const API = (url) => {
    return fetch(url, {
      method: "GET",
      headers: {
          "Content-Type": "application/json"
      },
    })
  };

  function reducer(state, action) {
    if (action.type =='load_data'){
      return {
        fetchedData: action.data,
        isLoading: true
      }
    }
  }

  const Profile = () => {
    const [state, dispatch] = useReducer(
      reducer,
      { isLoading: false, fetchedData: null }
    );
    console.log("render Profile"); //срабатывает 2 раза
    useEffect(() => {
      (async () => {
        if (!state.isLoading){
          API("https://jsonplaceholder.typicode.com/todos/1")
            .then((response) => response.json())
            .then(data => {
                dispatch({ type: 'load_data', data })
            })
            .catch((err) => {
              console.log(err);
            });
          console.log("useEffect Profile: fetching"); //срабатывает 1 раза
        } else {
          console.log("useEffect Profile"); //срабатывает 1 раза
        }
      })()  
    }, [state]);

    return <pre>Какая-то верстка {
      state.isLoading 
        ? JSON.stringify(state.fetchedData, null, 2)
        : 'Loading...'
    }</pre>
  }

  ReactDOM.render(<React.StrictMode><Profile /></React.StrictMode>, document.getElementById("root"));
</script>

В итоге наблюдаем следующую картину:

  • render Profile (монтирование и первый рендер)
  • useEffect Profile: fetching (причина: инициализация state, загрузка данных)
  • render Profile (причина: обновление state )
  • useEffect Profile (причина: обновление зависимости state)

В итоге все становится на свои места, надеюсь мне удалось объяснить что проиходит, на счет рефакторинга не настаиваю, но по опыту скажу что есть практики которые изначально ведут к дальнейшим проблемам. Надеюсь, мой ответ окажется полезным.

P.S. Что касается строгого режима или React.StrictMode, то его лучше использовать, он может помочь находить косяки, до того как они станут болью,но в текущем вопросе он не при чем, и но нужно учитывать:

Проверки строгого режима работают только в режиме разработки; они не оказывают никакого эффекта в продакшен-сборке.