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.

ja utilizei ele, o useApi tbm é bom, mas eu decidi fazer o meu pois eu implementei um sistema de usar toast dentro da requisição
vc pode usar o select para colocar o Toast :) Mas implementar sempre pega putros edge cases neh

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?

eu testei com 10 requsições com um servidor local cada um com tempo de resposta diferente, todas no mesmo useEffect, e cada uma renderizando um bloco diferente e funcionou bem, não trava a aplicação, fica com o Skeleton só no respectivo bloco que está carregando
interessante, boa!
a questão é que você falou de fontes diferentes, dai teria que fazer outro interceptor, é uma boa questão vou estar fazendo uns testes aqui