Бесконечный ре-рендер в useEffect при try finally
При загрузке страницы уходит асинхронный запрос. Полученные данные отправляются в стор Редакса. Чтобы сделать UX лучше, добавил прелоадеры...но тогда получается бесконечный ре-рендер компонентов. В чем здесь ошибка и как это можно поправить?
useEffect(() => {
async function getMovie() {
try {
dispatch(showPreloader());
const data = await getMovieData<IMovieData>(filmId);
dispatch(getMovieAction(data));
} catch (err) {
console.log(err);
} finally {
setTimeout(() => dispatch(hidePreloader()), 500);
}
}
getMovie();
}, [dispatch, filmId]);
Ответы (2 шт):
Ошибка в том, что Вы передаете dispatch в useEffect. Хук эффекта срабатывает каждый раз, когда меняются значения в переданном массиве. По сути, каждый раз вы даете ему новый объект, которым является dispatch. В принципе, не передавайте в useEffect объекты - он не умеет их сравнивать так, как Вы бы того хотели... В общем я советую чуть больше почитать про хуки.
Если Вы хотите, чтобы useEffect сработал один раз - передайте туда пустой массив - useEffect(callback, []). Колбек выполнится один раз после отрисовки компонента. Если Вам нужно, чтобы useEffect срабатывал каждый раз, когда меняется filmId - передавайте filmId - useEffect(callback, [filmId])
Я предлагаю Вам зависть timerId для setTimeout и пересмотреть логику для создания обещания для скрытия загрузчика, для улучшения еще рекомендую AbortController, в этом примере нет бесконечного rendera, может он Вам сможет помочь.
import React, { useReducer, useEffect } from "react";
import type { Reducer } from "react";
/* #region api.ts */
const API = {
getById: async (id: number, abortController: AbortController) => {
return await (
await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
signal: abortController.signal,
})
).json();
},
};
/* #endregion */
/* #region types.ts */
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
type SetPostAction = { type: "SET_DETAILS"; payload: Post };
type Action =
| SetPostAction
| { type: "CLEAR_DETAILS" }
| { type: "SHOW_LOADER" }
| { type: "HIDE_LOADER" }
| { type: string };
interface Store {
isLoading: boolean;
details: Post | null;
}
const defaultStore: Store = {
isLoading: false,
details: null,
};
interface Props {
postId: number;
}
interface IGetPostArgs {
postId: number;
abortController: AbortController;
dispatch: React.Dispatch<Action>;
}
/* #endregion */
/* #region reduce.ts */
const reduce: Reducer<Store, Action> = (state: Store, action: Action) => {
if (action.type === "SET_DETAILS") {
return { ...state, details: (action as SetPostAction).payload };
}
if (action.type === "CLEAR_DETAILS") {
return { ...state, details: null };
}
if (action.type === "SHOW_LOADER") {
return { ...state, isLoading: true };
}
if (action.type === "HIDE_LOADER") {
return { ...state, isLoading: false };
}
return state;
};
const actions = Object.freeze({
showLoader: () => ({ type: "SHOW_LOADER" }),
hideLoader: () => ({ type: "HIDE_LOADER" }),
clearDetails: () => ({ type: "CLEAR_DETAILS" }),
setDetails: (payload: Post) => ({ type: "SET_DETAILS", payload }),
});
/* #endregion */
/* #region utils.ts */
async function getPost(args: IGetPostArgs) {
const { abortController, postId, dispatch } = args;
dispatch(actions.showLoader());
let isRequestLoading = false;
let timerId: NodeJS.Timeout | undefined = undefined;
const promiseClose = () => {
if (timerId) clearTimeout(timerId);
if (!isRequestLoading) {
return dispatch(actions.hideLoader());
}
timerId = setTimeout(promiseClose, 500);
};
try {
isRequestLoading = true;
promiseClose();
const data = await API.getById(postId, abortController);
dispatch(actions.setDetails(data));
isRequestLoading = false;
} catch (err) {
console.log(err);
isRequestLoading = false;
dispatch(actions.clearDetails());
}
}
/* #endregion */
/* #region React Component */
const Questions1319143 = (props: Props) => {
const { postId } = props;
const [store, dispatch] = useReducer(reduce, defaultStore);
const { details, isLoading } = store;
useEffect(() => {
const abortController = new AbortController();
getPost({ dispatch, postId, abortController });
return () => {
abortController.abort();
};
}, [dispatch, postId]);
return <div>{isLoading ? "loading" : JSON.stringify(details)}</div>;
};
Questions1319143.defaultProps = {
postId: 1,
};
/* #endregion */
export { Questions1319143 };