Постараюсь это объяснить в два этапа, во втором со снипетом, предпочтительнее создавать именно снипеты, тем более сайт это позволяет.
Я провел небольшой рефакторинг, который сохранил проблему но, на мой взгляд, писать лучше так, будет легче найти баги.
Первое, что видится, это то что Вы дергаете 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, то его лучше использовать, он может помочь находить косяки, до того как они станут болью,но в текущем вопросе он не при чем, и но нужно учитывать:
Проверки строгого режима работают только в режиме разработки; они не
оказывают никакого эффекта в продакшен-сборке.