T
TanStack8mo ago
evident-indigo

Should queryFn(fetcher) be independent of React life cycle?

I'm working on a Next.js 14 project (App Router) and need to set up a centralized axios instance as queryFn(fetcher) in order to use react-query, which should integrates seamlessly with: 1. useSession from next-auth 2. i18n from next-intl: 3. router from next/router 4. A React modal component I have come up with two approaches: 1. A function returned from a custom React hook
const createAxiosInstance = ({ t, router, session }): AxiosInstance => {
const instance = axios.create({ baseURL: "/api" });

instance.interceptors.request.use((config) => {
const token = session.data?.token;
if (token) config.headers["Authorization"] = `Bearer ${token}`;
return config;
}, (error) => Promise.reject(error));

instance.interceptors.response.use((response) => response, (error) => {
if (error.response?.status === 401) router.push("/login");
return Promise.reject(error);
});

return instance;
};

const useRequest = () => {
const t = useTranslations("errorMessages");
const session = useSession();
const router = useRouter();
const instance = useMemo(() => createAxiosInstance({ t, session, router }), [t, session, router]);

return instance;
};
const createAxiosInstance = ({ t, router, session }): AxiosInstance => {
const instance = axios.create({ baseURL: "/api" });

instance.interceptors.request.use((config) => {
const token = session.data?.token;
if (token) config.headers["Authorization"] = `Bearer ${token}`;
return config;
}, (error) => Promise.reject(error));

instance.interceptors.response.use((response) => response, (error) => {
if (error.response?.status === 401) router.push("/login");
return Promise.reject(error);
});

return instance;
};

const useRequest = () => {
const t = useTranslations("errorMessages");
const session = useSession();
const router = useRouter();
const instance = useMemo(() => createAxiosInstance({ t, session, router }), [t, session, router]);

return instance;
};
2. A standalone, React-independent function
import axios from "axios";
const axiosInstance = axios.create({ baseURL: "/api" });

axiosInstance.interceptors.request.use(async (config) => {
const session = await getSession();
const token = session?.data?.token;
if (token) config.headers["Authorization"] = `Bearer ${token}`;
return config;
}, (error) => Promise.reject(error));

axiosInstance.interceptors.response.use((response) => response, (error) => {
if (error.response?.status === 401) window.location.href = "/login";
return Promise.reject(error);
});

export default axiosInstance;
import axios from "axios";
const axiosInstance = axios.create({ baseURL: "/api" });

axiosInstance.interceptors.request.use(async (config) => {
const session = await getSession();
const token = session?.data?.token;
if (token) config.headers["Authorization"] = `Bearer ${token}`;
return config;
}, (error) => Promise.reject(error));

axiosInstance.interceptors.response.use((response) => response, (error) => {
if (error.response?.status === 401) window.location.href = "/login";
return Promise.reject(error);
});

export default axiosInstance;
What do you guys think? What's the right way to go about it?
6 Replies
correct-apricot
correct-apricot8mo ago
Definitely #2. You might need to fire an XHR request outside of React, and there's no reason to trap yourself into a react hook.
evident-indigo
evident-indigoOP8mo ago
But I have some trouble getting the token using getSession from next-auth. For every outgoing request I make, it also makes an api call to my nextjs server for getting the session.
correct-apricot
correct-apricot8mo ago
I'm not super familiar with Next, but I would suggest handling the session itself within React. Wrap your app in a context provider, and have that context intercept the axios requests and manage session state within React.
evident-indigo
evident-indigoOP8mo ago
but I need to add the token(from session) to the header of every request sent to API server(separated from nextjs server). If my fetcher is independent of react, then I won't be able to get the session from context provider
correct-apricot
correct-apricot8mo ago
Let me grab a copy of our implementation
// RestClientPort.ts
import rateLimit from 'axios-rate-limit';
import axios, { isAxiosError } from 'axios';
import { inRange } from 'lodash';
import { GameplayError } from '../errors/GameplayError';
import { HTTPError } from '../errors/HTTPError';

// config constants
const MAX_SIMULTANEOUS_REQUESTS_PER_SECOND = 2;

const axiosInstance = axios.create({
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
withCredentials: true,
withXSRFToken: true,
baseURL: import.meta.env.VITE_APP_URL,
});

const isTestEnvironment = import.meta.env.MODE === 'test';

// While running vitest, we don't want any rate limiting.
const RESTClientPort = isTestEnvironment ? axiosInstance : rateLimit(axiosInstance, { maxRPS: MAX_SIMULTANEOUS_REQUESTS_PER_SECOND });

/**
* Response errors have an uncertain shape, so we try to narrow the types for client consumption.
*/
RESTClientPort.interceptors.response.use(
(response) => response,
(error) => {
if (isAxiosError(error) && error.response && error.response.data) {
const { status } = error.response;
const { message, code, name, errors } = error.response.data;

// An controller abort or thrown exception.
// The backend has served a human-readable message, and possibly a helpful unique code.
if (inRange(status, 400, 500)) {
throw new GameplayError(message, status, code, name, errors);
}

// Something went wrong and no one's talking. The only data available is a status code.
throw new HTTPError(message, status);
}
return Promise.reject(error);
}
);

export { RESTClientPort };
// RestClientPort.ts
import rateLimit from 'axios-rate-limit';
import axios, { isAxiosError } from 'axios';
import { inRange } from 'lodash';
import { GameplayError } from '../errors/GameplayError';
import { HTTPError } from '../errors/HTTPError';

// config constants
const MAX_SIMULTANEOUS_REQUESTS_PER_SECOND = 2;

const axiosInstance = axios.create({
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
withCredentials: true,
withXSRFToken: true,
baseURL: import.meta.env.VITE_APP_URL,
});

const isTestEnvironment = import.meta.env.MODE === 'test';

// While running vitest, we don't want any rate limiting.
const RESTClientPort = isTestEnvironment ? axiosInstance : rateLimit(axiosInstance, { maxRPS: MAX_SIMULTANEOUS_REQUESTS_PER_SECOND });

/**
* Response errors have an uncertain shape, so we try to narrow the types for client consumption.
*/
RESTClientPort.interceptors.response.use(
(response) => response,
(error) => {
if (isAxiosError(error) && error.response && error.response.data) {
const { status } = error.response;
const { message, code, name, errors } = error.response.data;

// An controller abort or thrown exception.
// The backend has served a human-readable message, and possibly a helpful unique code.
if (inRange(status, 400, 500)) {
throw new GameplayError(message, status, code, name, errors);
}

// Something went wrong and no one's talking. The only data available is a status code.
throw new HTTPError(message, status);
}
return Promise.reject(error);
}
);

export { RESTClientPort };
This generates a RestClientPort object that can receive additional interceptors.
//AuthServiceFactory.ts

/**
* Create a stateless auth service that uses a bearer token kept in localStorage.
*/
function makeStatelessAuthService(axiosInstance: AxiosInstance, config?: AuthServiceConfigOptions) {
const bearerTokenRepository = new BearerTokenRepository();
const adapter = new AuthStatelessHTTPAdapter(bearerTokenRepository);
const service = new AuthService(adapter, config);
attachAuthInterceptors(axiosInstance, service);
attachBearerTokenInterceptors(axiosInstance, bearerTokenRepository);
return service;
}

/**
* Reset the Auth service if the client receives a 401 Unauthorized error.
*/
function attachAuthInterceptors(axiosInstance: AxiosInstance, authService: AuthService) {
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (isAxiosError(error) && error.status === HttpStatusCode.Unauthorized) {
authService.reset();
}
return Promise.reject(error);
}
);
}

/**
* Append the Bearer Token to every request header.
*/
function attachBearerTokenInterceptors(axiosInstance: AxiosInstance, tokenRepository: BearerTokenRepositoryInterface) {
axiosInstance.interceptors.request.use((request) => {
const token = tokenRepository.get();
if (token) {
request.headers.Authentication = `Bearer ${token.plain_text_token}`;
}
return request;
});
}
//AuthServiceFactory.ts

/**
* Create a stateless auth service that uses a bearer token kept in localStorage.
*/
function makeStatelessAuthService(axiosInstance: AxiosInstance, config?: AuthServiceConfigOptions) {
const bearerTokenRepository = new BearerTokenRepository();
const adapter = new AuthStatelessHTTPAdapter(bearerTokenRepository);
const service = new AuthService(adapter, config);
attachAuthInterceptors(axiosInstance, service);
attachBearerTokenInterceptors(axiosInstance, bearerTokenRepository);
return service;
}

/**
* Reset the Auth service if the client receives a 401 Unauthorized error.
*/
function attachAuthInterceptors(axiosInstance: AxiosInstance, authService: AuthService) {
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (isAxiosError(error) && error.status === HttpStatusCode.Unauthorized) {
authService.reset();
}
return Promise.reject(error);
}
);
}

/**
* Append the Bearer Token to every request header.
*/
function attachBearerTokenInterceptors(axiosInstance: AxiosInstance, tokenRepository: BearerTokenRepositoryInterface) {
axiosInstance.interceptors.request.use((request) => {
const token = tokenRepository.get();
if (token) {
request.headers.Authentication = `Bearer ${token.plain_text_token}`;
}
return request;
});
}
This may be a bit more formal than is strictly necessary, but I need this dang codebase to last 5+ years of active development
unwilling-turquoise
unwilling-turquoise8mo ago
Are you on next auth v5? I think number 1 is fine, the hook works since you have dependency on react hooks like the router

Did you find this page helpful?