Skip to content

Instantly share code, notes, and snippets.

@isocroft
Last active December 14, 2025 02:46
Show Gist options
  • Select an option

  • Save isocroft/fbb87a7f865e9f6dbff3c577f9cfb65b to your computer and use it in GitHub Desktop.

Select an option

Save isocroft/fbb87a7f865e9f6dbff3c577f9cfb65b to your computer and use it in GitHub Desktop.
Utility to help extract clear, meaningful, human-oriented error messages (as opposed to generic & cryptic error messages) from an axios error object on a web frontend application to aid debugging on the backend
import axios, { AxiosError, AxiosResponse, AxiosRequestConfig } from "axios";
const expandOnError = (errorMessage: string, errorCode?: string, errorResponse?: AxiosResponse) => {
if (errorMessage === "Network Error" && Boolean(!errorResponse)) {
/* @HINT: The message returned below simply means that a CORS error occured OR the name resolution failed */
return errorCode === undefined
? "We are currently updating our systems... Please try again later"
: "Your ISP seems to have some other network-related issues";
}
return "The browser restricted access to the server response! Please contact admin";
};
const isCORSViolation = (request: XMLHttpRequest, config?: AxiosRequestConfig) => {
const frontendURIHost = window.location.host;
const backendBaseURL = new URL(config?.baseURL || config?.url || "https://x.yz");
const backendURIHost = backendBaseURL.host;
const isCrossDomainRequest = backendURIHost !== "x.yz" && frontendURIHost !== backendURIHost;
let hasAccessControlOnOrigin = false;
const requestHasValidStatus = Boolean((request.status >= 200 && request.status <= 508));
const contentType = request.getResponseHeader("Content-Type");
if (isCrossDomainRequest) {
let allowedOrigin = "";
try {
if (request.withCredentials) {
allowedOrigin = request.getResponseHeader("Access-Control-Allow-Origin") || "";
}
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
} catch (_) { allowedOrigin = ""; }
if (allowedOrigin.trim() === "*") {
return true;
} else {
hasAccessControlOnOrigin = Boolean(allowedOrigin.trim() === backendBaseURL.origin.trim());
}
}
return ((isCrossDomainRequest && requestHasValidStatus) && (!hasAccessControlOnOrigin && contentType !== null));
};
export const getMessageFromAxiosError = (error: AxiosError) => {
let message = "";
const isNotCORSViolation = !isCORSViolation(error?.request as XMLHttpRequest, error?.config);
/* @CHECK: https://axios-http.com/docs/handling_errors */
/* @SEE: https://www.intricatecloud.io/2020/03/how-to-handle-api-errors-in-your-web-app-using-axios/ */
/* @HINT: These are the varying ranges of error(s) that can occur when making async HTTP requests */
const isServerResponseEmpty = Boolean(!error?.response) && ((Boolean(error?.request) && isNotCORSViolation) && error?.code !== "ERR_NETWORK");
const isServerTimedOut = error?.code === "ECONNABORTED" || error?.code === "ETIMEDOUT";
const isServerUnreachable = error?.code === "ECONNREFUSED";
const isClientOffline = window !== undefined ? !window.navigator.onLine : false;
const errorMessagesMap = {
unreachable: "Our systems are unreachable! Please try again later",
timeout: "Our systems request(s) timed out! Please try again",
indeterminate: "Something went wrong! Please try again",
offline: "Your internet is unstable! Please check and try again"
};
switch (true) {
case isClientOffline:
message = errorMessagesMap["offline"];
break;
case isServerUnreachable:
message = errorMessagesMap["unreachable"];
break;
case isServerTimedOut:
message = errorMessagesMap["timeout"];
break;
case isServerResponseEmpty:
message = "Our systems response returned empty! Please try again";
break;
default:
message = Boolean(error?.response)
? errorMessagesMap["indeterminate"]
: expandOnError(error.message, error?.code, error?.response);
}
return message;
};
@isocroft

isocroft commented Feb 1, 2023

Copy link
Copy Markdown
Author
/* @USAGE: This is an example of how to use the above code */
    
import { AxiosError, AxiosHeaders } from "axios";
import type { AxiosResponse, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
import { getMessageFromAxiosError } from "getMessageFromError";

import { useState, useMemo, useRef, useEffect } from "react";
import {
  useBrowserScreenActivityStatusMonitor,
  useBrowserStorageEffectUpdates,
  useEffectCallback,
  useBrowserStorage
} from "react-busser";


export const JWT_CACHE_STRATEGY = {
  COOKIE_MODE: "TOKEN_IN_COOKIE",
  STORAGE_MODE: "TOKEN_IN_STORAGE"
};

const useJWTManager_Axios = <RefreshTokenRequest extends Record<string & {}, unknown>, FetchTokenRequest extends Record<string & {}, unknown>, RefreshTokenResponse = Record<string & {}, unknown>, FetchTokenResponse = unknown>({
    configs = {
      mode: JWT_CACHE_STRATEGY.STORAGE_MODE,
      TOKEN_KEYNAME: "access_token",
      R_TOKEN_KEYNAME: "refresh_token"
    },
  }: { configs: {
    mode: string,
    TOKEN_KEYNAME: string,
    R_TOKEN_KEYNAME: string
    } }) => {
      const { mode, TOKEN_KEYNAME, R_TOKEN_KEYNAME  } = configs;
      const isRefreshingToken = useRef<boolean>(false);
      const { getFromStorage, clearFromStorage } = useBrowserStorage({ storageType: "local" });
      const [token, setToken] = useBrowserStorageEffectUpdates<string | null>(
        TOKEN_KEYNAME,
        null,
        "local",
        mode === JWT_CACHE_STRATEGY.COOKIE_MODE ? "bypassEffect" : "enforceEffect"
      );
      const [ method_RefreshToken, url_RefreshToken ] = ["post", import.meta.env.VITE_AUTH_REFRESH_API_ENDPOINT];
      const [ method_ResetRefreshToken, url_ResetRefreshToken ] = ["post", import.meta.env.VITE_AUTH_LOGOUT_API_ENDPOINT];
      const [ method_FetchToken, url_FetchToken ] = ["post", import.meta.env.VITE_AUTH_LOGIN_API_ENDPOINT];
      
     const redirectToLoginPage = () => {
        if (window.location.pathname !== import.meta.env.VITE_LOGIN_PATHNAME) {
          window.location.assign(`${import.meta.env.VITE_LOGIN_PATHNAME}?redirectBack=${encodeURIComponent(window.location.href)}`);
        }
     };
     const $fetchToken = <FetchTokenRequest>(payload: FetchTokenRequest, url = "/", method = "post"): Promise<void> => {
       const onFetchTokenFulfilled = (payload: AxiosResponse<FetchTokenResponse>) => {
          if (mode === JWT_CACHE_STRATEGY.STORAGE_MODE) {
            const tokens = payload.data['data' as keyof FetchTokenResponse] as { [key: string]: unknown };
            setToken(tokens[TOKEN_KEYNAME] as string);
            setToStorage(R_TOKEN_KEYNAME, tokens[R_TOKEN_KEYNAME] as string);
          }
       };
       return axios[method.trim() as "post"](url.trim(), payload).then(onFetchTokenFulfilled);
     };
     
     const $refreshToken = <RefreshTokenRequest>(payload: RefreshTokenRequest, url = "/", method = "post"): Promise<string> => {
       if (isRefreshingToken.current) {
           return Promise.reject(new Error('cannot initiate refresh while existing refresh is ongoing'));
       }

       isRefreshingToken.current = true;

       const onRefreshTokenFulfilled = (payload: AxiosResponse<RefreshTokenResponse>) => {
        isRefreshingToken.current = false;
        if (mode === JWT_CACHE_STRATEGY.STORAGE_MODE) {
            let newAccessToken = payload.data[TOKEN_KEYNAME as keyof RefreshTokenResponse] as string;
            setTimeout(setToken, 0, newAccessToken);
            return newAccessToken;
        }
        return "";
       };
       return axios[method.trim() as "post"](url.trim(), payload).then(onRefreshTokenFulfilled, () => {
         isRefreshingToken.current = false;
         return "";
       });
     };
  
    const $releaseToken = (url = "/", method = "post"): Promise<void> => {
        const onReleaseTokenSettled = () => {
            if (mode === JWT_CACHE_STRATEGY.STORAGE_MODE) {
              clearFromStorage(TOKEN_KEYNAME);
              clearFromStorage(R_TOKEN_KEYNAME);
            }
        };
      return axios[method.trim() as "post"](url.trim()).then(onReleaseTokenSettled, onReleaseTokenSettled);
    };
  
    const reqInterceptorCallback = useEffectCallback((config: InternalAxiosRequestConfig & { _retry?: boolean }) => {
          if (mode === JWT_CACHE_STRATEGY.STORAGE_MODE && token !== null) {
            config.headers.Authorization = !config._retry && token
               ? `Bearer ${token}`
               : config.headers.Authorization
           }
           return config;
     }, { immutableRef: true });
    
    const [requestInterceptorId] = useState(() => {
       return axios.interceptors.request.use(
         reqInterceptorCallback as (value: InternalAxiosRequestConfig<any>) => InternalAxiosRequestConfig<any>,
         (error: AxiosError) => {
         return Promise.reject(error);
       });
    }); 
     
    const [responseInterceptorId] = useState(() => {
        return axios.interceptors.response.use(
            async function (response: AxiosResponse) {
                const statusCode = response?.status || 0;
                const isRejectedRequest = statusCode === 401 || statusCode === 403 || statusCode === 412;
                const isFailedRequest = statusCode === 400 || statusCode === 429 || statusCode >= 500;
    
                if (isRejectedRequest || isFailedRequest) {
                    return Promise.reject(new AxiosError(
                      `Something went wrong: ${response?.statusText || "Unknown Failure"}`,
                      "ERR_REQUEST_FAILED",
                      undefined,
                      undefined,
                      response
                    ));
                }
  
               if (!response) {
                 return Promise.reject(new AxiosError(
                    "The server returned an empty response! Please contact admin",
                    "ERR_EMPTY_RESPONSE",
                    undefined,
                    undefined,
                    {
                      status: 0,
                      statusText: "No Response Body",
                      headers: new AxiosHeaders({}),
                      config: { headers: new AxiosHeaders({}) },
                      request: undefined,
                      data: null
                    }
                  ));
                }
    
                return response;
            },
            async function (error: AxiosError) {
                const originalRequestConfig = Object.assign({ _retry: false, url: '/' }, error?.config || { headers: new AxiosHeaders({}), url: '/' });
                const originalResponse  = error?.response || { status: 0, statusText: "No Response Body", config: originalRequestConfig, headers: new AxiosHeaders({}), data: null };
                const errorMessage = getMessageFromAxiosError(error);
                const refreshToken = getFromStorage<string>(R_TOKEN_KEYNAME, "");
    
                if (window.location.pathname !== import.meta.env.VITE_LOGIN_PATHNAME) {
                    if (originalResponse.status === 401
                            && !originalRequestConfig._retry
                                && originalRequestConfig.url !== import.meta.env.VITE_AUTH_LOGOUT_API_ENDPOINT) {
                        return $refreshToken(
                            { refresh_token: refreshToken },
                            url_RefreshToken,
                            method_RefreshToken
                        ).then((newAccessToken) => {
                            if (mode === JWT_CACHE_STRATEGY.STORAGE_MODE) {
                                originalRequestConfig._retry = true;
                                originalRequestConfig.headers.Authorization = `Bearer ${newAccessToken}`;
                            }
                            return $axios(originalRequestConfig);
                        }).catch(() => {
                            return $releaseToken(url_ResetRefreshToken, method_ResetRefreshToken).then(redirectToLoginPage, redirectToLoginPage);
                        });
                    }
                }
               
               if (errorMessage === "") {
                 return Promise.reject(error);
               }

               if (errorMessage === "Our systems response returned empty! Please try again"
                 || errorMessage === "Our systems request(s) timed out! Please try again"
                 || (errorMessage === "Something went wrong! Please try again" && originalResponse.status !== 429)) {
               const headers = originalResponse?.headers;
               let X_Ratelimit_Remaining = 0;
               let X_Ratelimit_Limit = 0;
  
               if (headers instanceof AxiosHeaders) {  
                 if (headers.has('X-Ratelimit-Remaining')) {
                   X_Ratelimit_Remaining = Number(headers['X-Ratelimit-Remaining'] || "0");
                 }
                 
                 if (headers.has('X-Ratelimit-Limit')) {
                   X_Ratelimit_Limit = Number(headers['X-Ratelimit-Limit'] || "0");
                 }
               }

               if (X_Ratelimit_Limit - X_Ratelimit_Remaining > 0) {
                 originalRequestConfig.headers['X-Request-Retry'] = `timestamp=${(new Date()).getTime()}`;
                 return axios(originalRequestConfig);
               }
             }
               
                return Promise.reject(new AxiosError(
                  errorMessage,
                  error?.code || "ERR_UNKNOWN",
                  originalRequestConfig,
                  error?.request,
                  originalResponse
                ));
            });
       });
       
       useEffect(() => {
          return () => {
             $axios.interceptors.response.eject(responseInterceptorId);
             $axios.interceptors.request.eject(requestInterceptorId);
          }
       /* eslint-disable-next-line react-hooks/exhaustive-deps */
       }, []);
       
       return useMemo(() => {
         return {
           redirectToLoginPage,
           fetchToken (payload: FetchTokenRequest) {
             return $fetchToken(payload, url_FetchToken, method_FetchToken);
           },
           refreshToken (payload: RefreshTokenRequest) {
             return $refreshToken(payload, url_RefreshToken, method_RefreshToken);
           },
           releaseToken () {
             return $releaseToken(url_ResetRefreshToken, method_ResetRefreshToken);
           }
         } as const;
       }, []);
};
 
const useJWTExpiryMonitor = <RefreshRequestData extends Record<string & {}, unknown>, RefreshResponseData extends object, LoginRequestData extends Record<string & {}, unknown>, LoginResponseData extends object>({
    TOKEN_ISSUE_TIME_KEYNAME = "",
    TOKEN_EXPIRE_TIME_KEYNAME = "",
    TOKEN_KEYNAME = "access_token",
    R_TOKEN_KEYNAME = "refresh_token",
    MODE = JWT_CACHE_STRATEGY.STORAGE_MODE
  }, {
    LAST_ACTIVE_DURATION_THRESHOLD = 9500,
    ACTIVITY_TIMEOUT_DURATION = 6000
  }) => {
    const { refreshToken, releaseToken, fetchToken, redirectToLoginPage } = useJWTManager_Axios<RefreshRequestData, LoginRequestData, RefreshResponseData, LoginResponseData>({ configs: { mode: MODE,  TOKEN_KEYNAME,  R_TOKEN_KEYNAME } });
    const { setToStorage, getFromStorage, clearFromStorage } = useBrowserStorage({ storageType: "session" });
  
    const isTokenExpired = (issuedTime: number, expiryTime: number): boolean => {
      const nowTime = new Date().getTime();
      const $issueTime = new Date(
        issuedTime || nowTime,
      ).getTime();
      const $expireTime = new Date(
        expiryTime || nowTime,
      ).getTime();
  
      const tokenExpiryStatus = (nowTime + $issueTime) > $expireTime;
      return tokenExpiryStatus;
    };
  
    const checkTokenExpiryAndRefreshToken = (
      $refreshToken: () => Promise<AxiosResponse<RefreshResponseData>>,
    ) => {
      const tokenExpired = isTokenExpired(
        Number(getFromStorage(TOKEN_ISSUE_TIME_KEYNAME)),
        Number(getFromStorage(TOKEN_EXPIRE_TIME_KEYNAME)) - 3600
      );
  
      if (tokenExpired) {
          return $refreshToken().then(() => {
            const { tokenIssueTime, tokenExpiryTime } = {}; //@TODO: Get from `document.cookie` using a library
            setToStorage(TOKEN_ISSUE_TIME_KEYNAME, String(tokenIssueTime));
            setToStorage(TOKEN_EXPIRE_TIME_KEYNAME, String(tokenExpiryTime));
          });
      }
  
      return false;
    }; 
  
     const checkTokenExpiryAndDestroyToken = (
       $destroyToken: () => Promise<void>,
       tracker: { isUserAway: boolean, lastActive: number }
     ) => {
       const nowTime = new Date().getTime();
       const tokenExpired = isTokenExpired(
         Number(getFromStorage(TOKEN_ISSUE_TIME_KEYNAME)),
         Number(getFromStorage(TOKEN_EXPIRE_TIME_KEYNAME)) - 3600
        );
        const canDestroyAccessToken = tracker.isUserAway
          && nowTime - tracker.lastActive > LAST_ACTIVE_DURATION_THRESHOLD;
  
        if (tokenExpired) {
          if (canDestroyAccessToken) {
            return $destroyToken().then(() => {
              clearFromStorage(TOKEN_ISSUE_TIME_KEYNAME);
              clearFromStorage(TOKEN_EXPIRE_TIME_KEYNAME);
  
              if (!window.confirm(
                'Your login session has expired and you are currently logged out. Would you like to continue a new session?')) {
                    window.close();
              } else {
                redirectToLoginPage();
              }
            });
          }
        }
     };
     
     useBrowserScreenActivityStatusMonitor({
       ACTIVITY_TIMEOUT_DURATION,
       onPageNowActive (tracker) {
         checkTokenExpiryAndDestroyToken(releaseToken, tracker);
         window.dispatchEvent(new Event("screeninactivtystart"))
       },
       onPageNotActive (tracker) {
         checkTokenExpiryAndRefreshToken(refreshToken, tracker);
         window.dispatchEvent(new Event("screeninactivtyend"))
       },
       onPageVisible (tracker) {
         if (tracker.isUserAway) {
           checkTokenExpiryAndDestroyToken(releaseToken, tracker);
         }
         window.dispatchEvent(new Event("screentabhiddenend"))
       },
       onPageHidden (tracker) {
         if (!tracker.isUserAway) {
           checkTokenExpiryAndRefreshToken(refreshToken, tracker);
         }
         window.dispatchEvent(new Event("screentabhiddenstart"))
       },
     });
     
     return useMemo(() => {
       return {
          loginMutationFn (payload: LoginRequestData) {
            return fetchToken(payload);
          },
          logoutMutationFn () {
            return releaseToken().then(() => {
              clearFromStorage(TOKEN_ISSUE_TIME_KEYNAME);
              clearFromStorage(TOKEN_EXPIRE_TIME_KEYNAME);
              redirectToLoginPage();
            });
          }
       } as const;
     }, []);
};

@isocroft

Copy link
Copy Markdown
Author

NOTE: To use the function above correctly and properly, you have to expose extra response headers to the client (javascript) using Access-Control-Expose-Headers response header on the server-side and set it to contain Access-Control-Allow-Origin.

  Access-Control-Expose-Headers: Vary, Set-Cookie, Access-Control-Allow-Origin

@isocroft

isocroft commented Dec 3, 2025

Copy link
Copy Markdown
Author
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { /* API_BASE_URL, */ QueryKey, JWT_CACHE_STRATEGY } from '../utils/constants';

import type { loginRequest, loginResponse } from '../models/auth';

export const useAuthLogin = () => {
  const { tokenStorageMode, tokenStorageKeys } = useAuthConfig();
  const { loginMutationCallback } = useAuthStatus();
  const { setToStorage } = useBrowserStorage({ storageType: "local" });
  const queryClient = useQueryClient();

  const { mutate: login, isPending: isLoading, ...rest } = useMutation({
    mutationFn: loginMutationCallback,
    onSuccess: async (payload) => {
      if (payload.status === 200) {
        await queryClient.setQueryData([QueryKey.AuthPayload], payload.data);
      }
    }
  });
  
  return { login, isLoading, ...rest };
};

export const useAuthLogout = () => {
  const navigate = useNavigate();
  const { logoutMutationCallback } = useAuthStatus();
  const { mutate: logout, isPending: isLoading, ...rest } = useMutation({
    mutationFn: logoutMutationCallback,
    onSuccess: async (payload) => {
      navigate('/login');
    }
  });
  
  return { logout, isLoading, ...rest };
};

export const useAuthCheck = () => {
  const { checkIfUserAuthValid, getAuthUser } = useAuthStatus();
};

const {
  tokenStorageMode,
  tokenStorageKeys
} = useAuthConfig();

const {
  checkIfUserAuthValid,
  getAuthUser,
  setAuthUser,
  logoutMutationCallback,
  loginMutationCallback
} = useAuthStatus();

const {
  login,
  isLoading
} = useAuthLogin(); -> useAuthStatus() + useAuthConfig();

const {
  logout,
  isLoading
} = useAuthLogout(); -> useAuthStatus() + useAuthConfig();

@isocroft

isocroft commented Dec 3, 2025

Copy link
Copy Markdown
Author
import React, { createContext, useContext, useState, useRef, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';

import type { UseQueryOptions } from "@tanstack/react-query";
import type { AxiosResponse } from 'axios';
import type { TUser } from '../utils/models';
import type { loginRequest, loginResponse } from '../models/auth';

import { axios } from './axiosPoweredClient';
import { useJWTExpiryMonitor } from '../hooks/auth';

import { QueryKey, JWT_CACHE_STRATEGY } from '../utils/constants';


const useGetUser = ({ enabled, onSuccess, onError }: {
  enabled: boolean,
  onSuccess: UseQueryOptions<TUser>['onSuccess'],
  onError: UseQueryOptions<TUser>['onError']
}) => {
  return useQuery({
    queryKey: [QueryKey.UserProfile],
    queryFn: async (): Promise<AxiosResponse<TUser>> => {
      const response = await axios.get(import.meta.env.VITE_AUTH_USER_API_ENDPOINT);

      if(response.status === 200) {
        return response;
      } else {
        return Promise.reject(new Error(response.data.message));
      }
    },
    enabled,
    onSuccess,
    onError
  });
};

type AuthStatusContextDetails = {
  getAuthUser: () => TUser | null,
  setAuthUser: (user: TUser | null) => boolean,
  loginMuationCallback: (payload: loginRequest) => Promise<loginResponse>,
  logoutMutationCallback: () => Promise<void>,
  checkIfUserAuthValid: () => boolean
};

type AuthConfigContextDetails = {
 tokenStorageMode: typeof JWT_CACHE_STRATEGY[keyof typeof JWT_CACHE_STRATEGY],
 tokenStorageKeys: {
   TOKEN_KEYNAME: string,
   R_TOKEN_KEYNAME: string,
   USER_KEYNAME: string,
   TOKEN_ISSUE_TIME_KEYNAME: string,
  TOKEN_EXPIRE_TIME_KEYNAME: string
 }
};

// Create separate contexts for status and config to optimize re-renders
const AuthConfigContext = createContext<AuthConfigContextDetails>({
  tokenStorageMode: JWT_CACHE_STRATEGY.STORAGE_MODE,
  tokenStorageKeys: {
    TOKEN_KEYNAME: 'a_token',
    R_TOKEN_KEYNAME: 'r_token',
    USER_KEYNAME: 'u_details',
    TOKEN_ISSUE_TIME_KEYNAME: 'issd_at',
    TOKEN_EXPIRE_TIME_KEYNAME: 'exps_at'
  }
});

const AuthStatusContext = createContext<AuthStatusContextDetails>({
  getAuthUser: () => null,
  setAuthUser: (user = null) => false,
  logoutMutationCallback: () => Promise.resolve(undefined),
  loginMutationCallback: (payload = { email: '', password: '' }) => Promise.resolve({ data: { access_token: '', refresh_token: '' } }),
  checkIfUserAuthValid: () => false
});

/* eslint-disable-next-line react-refresh/only-export-components */
export const useAuthStatus = () => {
  const context = useContext(AuthStatusContext);
  if (context === undefined) {
    throw new Error('`useAuthStatus(...)` must be used within an AuthProvider');
  }
  return context;
};

/* eslint-disable-next-line react-refresh/only-export-components */
export const useAuthConfig = () => {
  const context = useContext(AuthConfigContext);
  if (context === undefined) {
    throw new Error('`useAuthConfig(...)` must be used within an AuthProvider');
  }
  return context;
};

interface AuthProviderProps {
  children: React.ReactNode;
  mode: AuthConfigContextDetails['tokenStorageMode'];
  keys: AuthConfigContextDetails['tokenStorageKeys'];
}

export const AuthProvider: React.FC<AuthProviderProps> = ({ children, mode, keys }) => {
  const navigate = useNavigate();
  const { getFromStorage, setToStorage } = useBrowserStorage({ storageType: "local" });
  const hasAuthTokenInStorage = Boolean(getFromStorage<string>(keys.TOKEN_KEYNAME));
  const lastAuthUserRefetchTime = useRef<number>(0);
  const { refetch, status, isInitialLoading, isPending, error } = useGetUser({
    onSuccess (data) {
      lastAuthUserRefetchTime.current = hasAuthTokenInStorage ? Date.now() : 0;
      setToStorage(storageKeys.USER_KEYNAME, data);
    },
    onError (error) {
      if (import.meta.env.DEV) {
        console.error(error.message);
      }
    },
    enabled: hasAuthTokenInStorage,
  });
  const {
       updateQueryCacheData,
       getDataFromCache,
   } = useReactQueryCache<TUser, Error>(
      { noRenderOnWrite:  false, queryKey: [QueryKey.UserProfile] },
      getFromStorage<TUser | undefined>(keys.USER_KEYNAME, undefined)
  );
  const { logoutMutationFn,  loginMutationFn } = useJWTExpiryMonitor({
    TOKEN_ISSUE_TIME_KEYNAME: keys.TOKEN_ISSUE_TIME_KEYNAME,
    TOKEN_EXPIRE_TIME_KEYNAME: keys.TOKEN_EXPIRE_TIME_KEYNAME,
    TOKEN_KEYNAME: keys.TOKEN_KEYNAME,
    MODE: mode
  });
  
  const checkIfAuthValid = () => {
    if (!hasAuthTokenInStorage || isInitialLoading || isPending) {
      return;
    }
    
    const now = Date.now();
    const threeMinutes = 180000;
    const canRefetch = now - lastAuthUserRefetchTime.current >= threeMinutes;
    if (canRefetch) {
      return refetch();
    }
    return Promise.reject(new Error("couldn't refetch"));
  };
  const stableLoginMutationCallback = useEffectCallback(loginMutationFn, { immutableRef: true });
  const stableLogoutMutationCallback = useEffectCallback(logoutMutationFn, { immutableRef: true });
  const stableCheckIfUserAuthValidRef = useEffectCallback(checkIfAuthValid, { immutableRef: true });
  
  useEffect(() => {
    const eventHandler = () => {
      stableCheckIfUserAuthValidRef()
    };
    window.addEventListener('refetchuserquery', eventHandler, false);
    eventHandler();
    
    const intervalId = window.setInterval(() => {
      if (mode === JWT_CACHE_STRATEGY.COOKIE_MODE) {
        stableCheckIfUserAuthValidRef()
      }
    }, 60500);
    
    return () => {
      window.clearInterval(intervalId);
      window.removeEventListener('refetchuserquery', eventHandler, false);
    };
  }, [mode]);
  
  const authStatusValue = useEffectMemo(() => {
    return {
      checkIfUserAuthValid () {
         window.dispatchEvent(new Event('refetchuserquery'))
      },
      setAuthUser (data: TUser) {
        return Boolean(updateQueryCacheData([QueryKey.UserProfile], () => data));
      },
      getAuthUser () {
        return getDataFromCache([QueryKey.UserProfile]) || null;
      },
      logoutMutationCallback () {
        return stableLogoutMutationCallback();
      },
      loginMutationCallback (...args) {
        return stableLoginMutationCallback(...args);
      }
    };
  }, []);
  
  const authConfigValue = useMemo(() => {
    return { tokenStorageMode: mode, tokenStorageKeys: keys };
  }, []);
  
  useEffect(() => {
    const hasAuthTokenInStorage = Boolean(getFromStorage<string>(keys.TOKEN_KEYNAME, undefined));

    if(hasAuthTokenInStorage && status === "success") {
      navigate(import.meta.env.VITE_LOGIN_REDIRECT_PATHNAME);
    }
  }, [status]);
  
  /* @NOTE: `isInitialLoading` which was added in v4.x.x is now currently deprecated in Tanstack React-Query v5.x.x */
  /* @CHECK: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery */
  
  /* @HINT: Used `isPending` instead of `isLoading` because it seems to always be `true` even when `useQuery(...)` is disabled */
  /* @CHECK: https://github.com/TanStack/query/issues/3975 + https://tanstack.com/query/v4/docs/guides/queries#why-two-different-states */
  if (isInitialLoading || isPending) return <Spinner />;
  if (error) return <Error message={error.message} />;
  
  return (
    <AuthConfigContext.Provider value={authConfigValue}>
      <AuthStatusContext.Provider value={authStatusValue}>
        {children}
      </AuthStatusContext.Provider>
    </AuthConfigContext.Provider>
  );
};

@isocroft

isocroft commented Dec 6, 2025

Copy link
Copy Markdown
Author

TanStack/query#6327 (reply in thread)

VITE_LOGIN_REDIRECT_PATHNAME=/dashboard
VITE_LOGIN_PATHNAME=/login
VITE_API_BASE_URL=https://slimy-quiet.pipeops.app/api/v1
VITE_API_TIMEOUT_SECONDS=10000
VITE_AUTH_USER_API_ENDPOINT=https://slimy-quiet.pipeops.app/api/v1/api/v1/users/me
VITE_AUTH_LOGIN_API_ENDPOINT=https://slimy-quiet.pipeops.app/api/v1/auth/login
VITE_AUTH_REFRESH_API_ENDPOINT=https://slimy-quiet.pipeops.app/api/v1/auth/refresh-token,
VITE_AUTH_LOGOUT_API_ENDPOINT=https://slimy-quiet.pipeops.app/api/v1/auth/logout

@isocroft

isocroft commented Dec 6, 2025

Copy link
Copy Markdown
Author
export const useRouteQueryPrefetch = ({ queryOptions, prefetchOnMount = false, cacheAndPersist = false }: { queryOptions: UseQueryOptions, prefetchOnMount: boolean, cacheAndPersist: boolean }) => {
    const navigate = useNavigate();
    const queryClient = useQueryClient();
    const [isPending, startTransition] = useTransition()
    const isFirstRender = useIsFirstRender();
  
    useEffect(() => {
      if (isFirstRender && prefetchOnMount) {
        /* @HINT:  Prefetch in background just after component mount */
        if (!cacheAndPersist) {
          queryClient.prefetchQuery(queryOptions);
        } else {
          try {
            /* @CHECK: https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientensurequerydata */
            queryClient.ensureQueryData(queryOptions);
            /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
          } catch (_) { /* eslint-disable no-empty */ }
        }
      }
    /* eslint-disable-next-line react-hooks/exhaustive-deps */
    }, [prefetchOnMount, cacheAndPersist, isFirstRender]);
    
    const handlePathNavigation = async (urlPathname = '/', queryOptions = { queryKey: [], queryFn: () => null }) => {
      /* @HINT: Prefetch during route navigation */
      await queryClient.prefetchQuery(queryOptions);
      navigate(urlPathname);
    };
    
    const onRouteNavigationTriggered = (e: React.MouseEvent<HTMLElement> & { target: HTMLElement }) => {
      if (isPending) {
        e.stopPropagation();
        e.preventDefault();
        return;
      }
      
      e.persist();
      
      startTransition(async () => {
        const { pathname } = new URL(e.target.getAttribute('data-href') || "/", window.location.origin);
        /* @ts-ignore */
        await handlePathNavigation(pathname, queryOptions);
      });
    };
  
    return {
      isRoutePrefetching: isPending,
      onRouteNavigation: onRouteNavigationTriggered
    } as const;  
  };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment