React: Set up axios to work with Suspense

111 Views Asked by At

I recently heard about React.Suspense and I wanted to use it in a new project.

To setup React.Suspense with Axios I followed this tutorial

The first request of in that project consist of checking if there is a user logged in to the app. I checked that by sending a request /user to the API and if the API returns the user information if there is a cookie containing a JWT token otherwise it will return a 401 error

The issue is that I am having errors that I don't understand.

Here is the code of some parts of the project, hoping that it can help you understand my issue

App.tsx

function App() {
  return (
    <BrowserRouter>
      <AxiosInstanceProvider config={{ baseURL: process.env.BACKEND_URL || "http://localhost:8080/" }}>
        <CurrentUserContextProvider>
          <Routes>
            <Route path='/' element={<LandingPage />} />
            <Route path='/auth/*' element={<AuthPage />}>
              <Route path='sign-in' element={<SignInPage />} />
              <Route path='sign-up' element={<SignUpPage />} />
            </Route>
            <Route path='/orgs' element={<ListOfOrganizationsPage />} />
          </Routes>
        </CurrentUserContextProvider>
      </AxiosInstanceProvider>
    </BrowserRouter>
  );
}

AxiosInstanceProvider

export const AxiosContext = createContext<AxiosInstance | null>(null);
export const useAxiosContext = () => useContext(AxiosContext);

interface AxiosInstanceProviderProps {
  config: CreateAxiosDefaults | AxiosRequestConfig,
  children: JSX.Element
}
export const AxiosInstanceProvider = ({
  config = {},
  children,
}: AxiosInstanceProviderProps) => {
  const instanceRef = useRef(
    axios.create({
      ...config,
      headers: {
        'Accept': 'application/json',
        "Content-Type": "application/json",
      },
      withCredentials: true
    })
  );
  const location = useLocation();
  const navigate = useNavigate();

  const redirectToSignIn = useCallback(() => {
    if (location.pathname.includes("/auth/sign-in")) return;

    navigate(`/auth/sign-in?from=${encodeURIComponent(location.pathname)}`)
  }, [location.pathname, navigate]);

  const onResponse = useCallback((response: AxiosResponse) => {
    return response;
  }, []);

  const onUnauthorizedRequest = useCallback((error: AxiosError) => {
    if (error?.response?.status === 401) {
      redirectToSignIn();

      return;
    }

    return Promise.reject(error)
  }, [redirectToSignIn]);


  useEffect(() => {
    instanceRef.current.interceptors.response.use(onResponse, onUnauthorizedRequest);
  }, [instanceRef, onResponse, onUnauthorizedRequest]);


  return (
    <AxiosContext.Provider value={instanceRef.current}>
      {children}
    </AxiosContext.Provider>
  );
}

useAxios

type RequestMethod = "GET" | "POST" | "PATCH" | "DELETE";

export class TError {
  error: string;

  constructor(message: string) {
    this.error = message;
  }
}

function wrapPromise<T>(promise: Promise<T>) {
  let status = 'pending'
  let response: T|string;

  const suspender = promise.then(
    (res: T) => {
      status = 'success'
      response = res
    },
  ).catch((err: AxiosError) => {
      console.log("an error occurent: ", err);
      status = 'error'
      response = err.response?.data as string;
    },
  )

  const read = () => {
    switch (status) {
      case 'pending':
        throw suspender
      case 'error':
        throw response
      default:
        return response
    }
  }

  return { read }
}

const useAxios = () => {
  const abortControllerRef = useRef(new AbortController());
  const contextAxiosInstance = useAxiosContext();
  const instance = useMemo(() => {
    return contextAxiosInstance || axios
  }, [contextAxiosInstance])
  const cancel = () => {
    abortControllerRef.current.abort();
  }

  const fetchApiResponse = useCallback(
    async <T,S = {}>(url: string, method: RequestMethod, data?: S) => {
      try {
        const response = await instance<T>({
          method,
          url,
          data
        })

        if (response.status >= 200 && response.status < 400) {
          return response;
        } else {
          throw new Error(response.statusText)
        }
      } catch (error) {
        return error as AxiosError;
      }
    },
    [instance]
  )

  const fetchApiData = useCallback(<T,S = {}>(url: string, method: RequestMethod, data?: S) => {
      const promise = instance<T>({
        method,
        url,
        data
      })
      .then((response) => response.data);

      return wrapPromise(promise);
    },
    [instance]
  )

  return useMemo(() => ({
    cancel,
    fetchApiResponse,
    fetchApiData
  }), [fetchApiData, fetchApiResponse]);

};

export default useAxios;

GetCurrentUser

export default function useApiAuth() {
  const { fetchApiData } = useAxios();

  const getCurrentUser = useCallback(
    () => fetchApiData<UserType>("user", "GET").read(),
    [fetchApiData]
  )

  return useMemo(
    () => ({
      getCurrentUser,
    }),
    [
      getCurrentUser
    ]
  )
}

CurrentUserContext

interface CurrentUserContextType {
  currentUser: UserType | null;
  isLoadingCurrentUser: boolean;
  setCurrentUser: (user: UserType) => void;
}
export const CurrentUserContext = createContext<CurrentUserContextType>({
  currentUser: null,
  isLoadingCurrentUser: false,
  setCurrentUser: () => {}
});
export const useCurrentUserContext = () => useContext(CurrentUserContext);

const CurrentUserContextProvider = ({ children }: CurrentUserContextProviderProps) => {
  const {currentUser, isLoadingCurrentUser, setCurrentUser} = useCurrentUser();

  return (
    <CurrentUserContext.Provider value={{currentUser, isLoadingCurrentUser, setCurrentUser}}>
      {children}
    </CurrentUserContext.Provider>
  )
}

interface CurrentUserContextProviderProps {
  children: JSX.Element
}
export default memo(({ children }: CurrentUserContextProviderProps) => {
  return (
    // <ErrorBoundary fallback={<div>Error Occurred when checking current user..</div>}>
      <Suspense fallback={<div>Checking current user ...</div>}>
        <CurrentUserContextProvider>
          {children}
        </CurrentUserContextProvider>
      </Suspense>
    // </ErrorBoundary>
  )
});

useCurrentUser

export default function useCurrentUser() {
  const [error, setError] = useState("");
  const [currentUser, setCurrentUser] = useState<UserType | null>(null);
  const [isLoadingCurrentUser, setIsLoadingCurrentUser] = useState<boolean>(true);
  const { getCurrentUser } = useApiAuth();

  useEffect(() => {
    function _getCurrentUser() {
      setIsLoadingCurrentUser(true);
      const response = getCurrentUser();
      console.log("GCUR-U : Response : ", response);
      if (response instanceof TError) {
        console.log("Response Error")
        setError(response.error);
      }
      setIsLoadingCurrentUser(false);
    }

    _getCurrentUser();
  }, [getCurrentUser, setIsLoadingCurrentUser, setError]);

  return useMemo(() => ({
    currentUser,
    setCurrentUser,
    isLoadingCurrentUser,
    error
  }), [currentUser, error, isLoadingCurrentUser]);
}

I was expecting to see the message Checking current user ... while sending the request /user to the API and then being redirect to /auth/sign-in But I am having those errors

  • Promise {<pending>}
  • The above error occurred in the

enter image description here

0

There are 0 best solutions below