Gerenciamento de Loadings globais em React
Recentemente eu estava fazendo uma aplcação que fazia muitas requisições externas e consequentemente eu tinha muitos gerenciamentos de estados para renderizar os conteúdos, por fim decidi centralizar tudo em um interceptor, então essa foi a minha abordagem.
Interceptor
Eu precisaria fazer algo pelo qual todas as minhas requisições fossem filtradas.
utilizei o interceptor do Axios que seria a biblioteca que eu utilizaria para fazer as requsições.
const axiosInstance = axios.create({
baseURL: "http://localhost:3000/api/",
});
const AxiosInterceptor = ({ children }) => {
const { addRequest, setLoaded } = useLoaded();
const [interceptorReady, setInterceptorReady] = useAtom(
interceptorRunningAtom
);
useEffect(() => {
const requestInterceptor = (request) => {
addRequest(request.headers.request_name);
return request;
};
const responseInterceptor = (response) => {
setLoaded(response.config.headers.request_name);
return response;
};
const errorInterceptor = (error) => {
setLoaded(error.config.headers.request_name);
return Promise.reject(error);
};
const interceptorOut = axiosInstance.interceptors.request.use(
requestInterceptor,
errorInterceptor
);
const interceptorIn = axiosInstance.interceptors.response.use(
responseInterceptor,
errorInterceptor
);
setInterceptorReady(true);
return () => {
axiosInstance.interceptors.request.eject(interceptorOut);
axiosInstance.interceptors.response.eject(interceptorIn);
};
}, []);
return interceptorReady ? children : <div>loading...<div>;
};
export default axiosInstance;
export { AxiosInterceptor };
No interceptorOut ele adiciona uma nova requisição a fila de loading através da função:
addRequest(request.headers.request_name);
No interceptorIn ele adiciona a fila de concluidos através da função.
setLoaded(response.config.headers.request_name);
essas funções são importadas do hook criado anteriormente que será abordado em seguida.
const { addRequest, setLoaded } = useLoaded();
e por fim é adicionado um "atom" da biblioteca jotai, para renderizar a aplicação só depois do interceptor estar configurado, pois senão funções que rodariam no primeiro ciclo de um UseEffect não funcionaria.
useLoaded
Criei um hook para gerenciar os loadings baseado ao nome que é dado para a requisição na hora da chamada.
import { useEffect } from "react";
import { useAtom, atom } from "jotai";
const pendingAtom = atom([]);
const finishedAtom = atom([]);
export const useLoaded = () => {
const [pending, setPending] = useAtom(pendingAtom);
const [finished, setFinished] = useAtom(finishedAtom);
useEffect(() => {
if (finished.length === 0) return;
let newPending = pending;
let newFinished = finished;
finished.forEach((request) => {
newPending = newPending.filter((t) => t.name !== request.name);
newFinished = newFinished.filter((t) => t.name !== request.name);
});
setPending(newPending);
setFinished(newFinished);
}, [finished]);
return {
loaded: (name) => {
if (pending.filter((request) => request.name === name)[0] === undefined)
return true;
let request = finished.filter((request) => request.name === name)[0];
return request?.loaded || false;
},
addRequest: (name) => {
setPending((prev) =>
prev.find((t) => t.name === name)
? prev
: [...prev, { name, loaded: false }]
);
},
setLoaded: (name) => {
setFinished((prev) =>
prev.find((t) => t.name === name)
? prev
: [...prev, { name, loaded: true }]
);
},
resetList: () => {
setFinished([]);
setPending([]);
},
};
};
Como explicado anteriormente o hook é bem simples de ser utilizado, quando for fazer uma requisição ele ja vai criar uma request no hook pois ela vai passar pelo interceptor.
Atenção
Para funcionar as requisições precisam passar um header com o nome da requisição, pois ele pode gerenciar diversas requsições no mesmo instante.
por exemplo uma requsição que pegaria as notificações de uma aplcação:
import axiosInstance from "./api";
export const getNotifications = async (request_name) => {
return await axiosInstance.get("notifications", {
headers: {
request_name: request_name,
},
});
};
Ajustar o APP
No root do seu projeto deverá ser colocado o interceptor para que todas as requisições passem por ele.
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<AxiosInterceptor>
<BrowserRouter>
<App />
</BrowserRouter>
</AxiosInterceptor>
</React.StrictMode>
);
Utilizando
Por fim, para utilizar o loader da requisição é bem simples primeiro na hora de chamar a requisição é só passar o nome da requisição.
const { loaded, resetList } = useLoaded();
const [notifications, setNotifications] = useState([]);
useLayoutEffect(() => {
resetList();
async function fetchNotifications() {
getNotifications("notifications").then((response) => {
setNotifications(response.data.notifications);
});
}
fetchNotifications();
}, []);
Então na hora de gerenciar o loading é só passar o mesmo nome para ele, aqui foi utilizado o Skeleton do chakra UI, então por exemplo:
<Skeleton
isLoaded={loaded("notifications")}
minHeight={"230px"}
width={"100%"}
>
{notifications.map((notification, index) => {
return(<p key={index}>{notification.title}</p>)
}
</Skeleton>
Eu curto react query , abstracao dele é otima. E uso o errorBoubday deles.
Um dúvida, se tivermos um dashboard, onde várias informações vem de fontes diferentes, o loading toma conta da aplicação inteira (travando o usuário) ou somente daquele bloco específico?