How to access useContext in Axios interceptor - reactjs

I have created an Axios instance and a notificationContext to use in a React webapp.
I want to access the addNotification function of the notificationContext inside the request/response interceptor of the created Axios instance but since this is not a React component it's not possible. Is there a way to make and use this created Axios instance and interceptors as a React component?
NotificationContext.js
import React, { createContext, useState } from 'react';
const NotificationContext = createContext();
const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([]);
const addNotification = (type, title, description) => {
const notification = {
id: Date.now(),
type,
title,
description,
};
setNotifications([notification, ...notifications]);
};
const removeNotification = (id) => {
setNotifications(notifications.filter((notification) => notification.id !== id));
};
return <NotificationContext.Provider value={{ notifications, addNotification, removeNotification }}>{children}</NotificationContext.Provider>;
};
export { NotificationContext, NotificationProvider };
App.js
The NotificationProvider is wrapped around the NotificationList component
const App = () => (
<BrowserRouter>
<AuthProvider>
<NotificationProvider>
<NotificationList />
<Router />
</NotificationProvider>
</AuthProvider>
</BrowserRouter>
);
export default App;
apiClient.js
import axios from 'axios';
// import { useContext } from 'react';
// import { NotificationContext } from '../context/NotificationContext';
const apiClient = axios.create({
baseURL: process.env.REACT_APP_API,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
withCredentials: true,
timeout: 10000,
});
apiClient.interceptors.request.use(
(config) => {
return config;
},
(error) => {
return Promise.reject(error);
// How to access AddNotification here
},
);
apiClient.interceptors.response.use(
(response) => {
return Promise.resolve(response);
},
(error) => {
if (!error.response) {
// network error
// console.log('Network error');
// How to access AddNotification here
} else {
const response = error.response.data;
// How to access AddNotification here
}
return Promise.reject(error);
},
);
export default apiClient;

You can export the axios instance
create a functional component
access the context
init interceptor in useMemo
Example (just for an idea):
function Interceptor({children}) {
const {addNotification} = useContext(NotificationContext)
useMemo(() => {
apiClient.interceptors.response.use(
(response) => {
return Promise.resolve(response);
},
(error) => {
if (!error.response) {
addNotification()
} else {
const response = error.response.data;
addNotification()
}
return Promise.reject(error);
},
);
}, [addNotification])
return <>{children}</>
}
and in App.js
const App = () => (
<BrowserRouter>
<AuthProvider>
<NotificationProvider>
<Interceptor> // here
<NotificationList />
<Router />
<Interceptor />
</NotificationProvider>
</AuthProvider>
</BrowserRouter>);
export default App;

Related

The react apollo useSubscription hook doesn't work in a function call but works when the same call is assigned to a variable

I am working with apollo graphql client. The subscription in the server is working fine watching for changes.
But in the client, I am not able to log data.
I also tried to mutate but still its resulting in the same thing.
useSubscription(BOOK_ADDED, {
onData: ({ data }) => {
console.log(data)
}
})
The above code doesn't log anything out.
But,
const value = useSubscription(BOOK_ADDED, {
onData: ({ data }) => {
console.log(data)
}
})
console.log(value)
The above code seems to work fine logging out a value.
I am attaching a few codes below for more clarity.
index.js or apollo setup:
import ReactDOM from 'react-dom'
import App from './App'
import {
ApolloClient,
ApolloProvider,
HttpLink,
InMemoryCache,
split,
} from '#apollo/client'
import { setContext } from '#apollo/client/link/context'
import { getMainDefinition } from '#apollo/client/utilities'
import { GraphQLWsLink } from '#apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
import Assess from './Asses'
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('library-user-token')
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
const httpLink = new HttpLink({ uri: 'http://localhost:4002' })
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4002',
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
authLink.concat(httpLink)
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
ReactDOM.render(
<ApolloProvider client={client}>
<Assess />
</ApolloProvider>,
document.getElementById('root')
)
App.js
//App.js
import { useSubscription, useQuery } from "#apollo/client";
import { ALL_BOOKS, BOOK_ADDED } from "./queries";
const App = () => {
console.log(BOOK_ADDED);
const result = useQuery(ALL_BOOKS);
useSubscription(BOOK_ADDED, {
onData: ({ data }) => {
console.log(data);
},
});
console.log(result)
if(result.loading){
return null
}
return (
<div>
{result?.data?.allBooks.map((r) => (
<li key={r.id}>{r.title}</li>
))}
</div>
);
};
export default App
The query and fragment:
const BOOK_DETAILS = gql`
fragment BookDetails on Books {
title
author {
name
}
published
genres
id
}
`;
export const BOOK_ADDED = gql`
subscription {
bookAdded {
...BookDetails
}
}
${BOOK_DETAILS}
`;
After reading the changelogs of #apollo/client. I got to know that the method demonstrated in question is only useful when the version of #apollo/client is >=3.7.0. As my version was #3.6.7 it wasn't logging the value out.
Earlier than this version the function required an onSubscriptionData callback
to perform the same operation, which is now deprecated. I have still demonstrated it below as someone using version <#3.7.0 might find it useful.
useSubscription(BOOK_ADDED,{
onSubscriptionData: ({subscriptionData: data}) =>{
console.log(data)
}
})
You may read the change log here.

Use effect not working in without refreshing the page

I am making a app using ionic-react. Calling a api in useEffect but api is not getting call.
import classes from './index.module.scss';
import React, { useEffect, useState } from 'react';
import { IonContent, IonPage } from '#ionic/react';
import { LoggedInHeader, WalletCard } from '../../components';
import { Stack } from '#mui/material';
import { wallet } from '../../apis';
interface WalletProps { };
export const Wallet = (props: WalletProps) => {
const [walletHistory, setWalletHistory] = useState<any[]>([]);
useEffect(() => {
console.log("is this comingggggggggggggg")
let data = {
"user_id": localStorage.getItem("userid")
}
wallet(data)
.then((response)=> {
setWalletHistory(response.data.data)
})
.catch((error)=>{
console.error("Getting error at auth: ", error);
})
}, [])
return (
<IonPage>
<IonContent fullscreen>
<LoggedInHeader />
some content
</IonContent>
</IonPage>
)
}
If i add a dependency in useeffect then api is calling infinite time.
Wallet api calling file
instance.interceptors.request.use(function (config: AxiosRequestConfig) {
const token = localStorage.getItem('usertoken') || '';
if (token) {
const headers: AxiosRequestHeaders = {
Authorization: token
};
config.headers = headers;
}
return config;
});
export const wallet = (user_id:any) => instance({
method: 'GET',
url: `/customer/wallet_history`,
data: user_id,
responseType: 'json'
});
This is my api calling file and i am using axios instance

Next.js: How to create get request using React Query

I want to integrate to React query for fetching the data from server.
So far I've been fetching the rest api via Axios.
I have created a custom hook for fetching and want to transform and implement with react query.
While trying to implement the same logic I encountered an error trying to destructure the fetched data const { data } = useApiRequest(headersUrl):
error - TypeError: (0 , _hooks_useApiRequest__WEBPACK_IMPORTED_MODULE_1__.UseApiRequest) is not a function
Here is the old logic for fetching
import * as React from "react";
import { useState, useEffect } from "react";
import axios from "axios";
import { HeaderToken } from "../services/api";
export const useApiRequest = (url: any) => {
const [data, setData] = useState<[] | any>([]);
useEffect(() => {
const fetchData = () => {
axios
.get(url, {
headers: {
Authorization: `Basic ${HeaderToken}`,
},
})
.then((response) => {
setData(response.data);
})
.catch((error) => console.error(error));
};
fetchData();
}, [url]);
return { data };
};
And here is how I'm trying to convert it:
import { HeaderToken } from "../services/api";
import { useQuery } from "react-query";
export const useApiRequest = (url: any) => {
const { isLoading, data } = useQuery("bc", async () => {
const response = await fetch(url, {
method: "get",
headers: {
Authorization: `Basic ${HeaderToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) throw new Error(response.statusText);
return await response.json();
});
return { data };
};
I can't see the problem, actually, I tried the same code you shared with a local API I have and it's working
The Hook
import { useQuery } from 'react-query'
export const clientAPI = (url) => {
const { isLoading, data } = useQuery("bc", async () => {
const response = await fetch(url, {
method: "get"
});
if (!response.ok) throw new Error(response.statusText);
return await response.json();
});
return { data };
};
React Component
import * as React from "react";
import { clientAPI } from "../hooks/clientAPI";
export default function Home() {
const { data } = clientAPI('http://localhost:5000/')
return (
<div>
{JSON.stringify(data)}
</div>
)
}
_app.js
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
function MyApp({ Component, pageProps }) {
return (<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>)
}
export default MyApp
I'm using next#11.1.2, react-query#3.28.0
what are the versions you are using?

React Custom Hooks Circular Dependency

I have two custom hooks i.e useFetch and useAuth. useAuth has all API calls methods (e.g logIn, logOut, register, getProfile etc) and they use useFetch hook method for doing API calls. useFetch also uses these methods for example logOut method when API return 401, setToken etc. So, they both need to share common methods. But that results into circular dependency and call size stack exceeded error. How to manage this
UseFetch.js
import React, { useState, useContext } from "react";
import { AuthContext } from "../context/authContext";
import { baseURL } from "../utils/constants";
import { useAuth } from "./useAuth";
const RCTNetworking = require("react-native/Libraries/Network/RCTNetworking");
export const useFetch = () => {
const {token, setAuthToken, isLoading, setIsLoading, logIn, logOut} = useAuth();
const fetchAPI = (method, url, body, isPublic, noBaseURL) => {
setIsLoading(true);
const options = {
method: method
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
};
return fetch(url, options, isRetrying).then(() => {
......
})
......
};
return { fetchAPI };
};
UseAuth.js
import React, { useContext, useEffect } from "react";
import { AuthContext } from "../context/authContext";
import { useFetch } from "./useFetch";
export const useAuth = () => {
const {
removeAuthToken,
removeUser,
setUser,
...others
} = useContext(AuthContext);
const { fetchAPI } = useFetch();
const register = (body) => {
return fetchAPI("POST", "/customers/register", body, true);
};
const logIn = (body) => {
return fetchAPI("POST", "/customers/login", body, true);
};
const logOut = () => {
return (
fetchAPI("POST", "/customers/logout")
.catch((err) => console.log("err", err.message))
.finally(() => {
removeAuthToken();
removeUser();
})
);
......
};
return {
...others,
register,
logIn,
logOut,
};
};

How to not call any api endpoints if firebase token is expired in React app?

I am using firebase-ui-react in a React app for authentication, but the problem is that if someone open the app and don't do any activity up to an hour and after one hour click on a link, all the endpoints get called several times until the the app crash.
the index.jsx looks like this:
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route } from "react-router-dom";
import { Provider } from "react-redux";
import * as Sentry from "#sentry/react";
import store from "./Redux/store";
import App from "./App";
import ReactGA from "react-ga";
import { AuthProvider } from "./Containers/Auth/AuthProvider";
import * as firebaseconfig from "./deployment_config.json";
const fbconfig =
firebaseconfig.default[process.env.REACT_APP_ENV_NAME][
process.env.REACT_APP_CLIENT
];
Sentry.init({ dsn: fbconfig.SENTRY_DSN });
ReactGA.initialize(fbconfig.GOOGLE_ANALYTICS_TRACKING);
ReactDOM.render(
<BrowserRouter>
<AuthProvider fbconfig={fbconfig}>
<Provider store={store}>
<App />
</Provider>
</AuthProvider>
</BrowserRouter>,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
the App.jsx looks like:
import React, { useState, useContext, useEffect } from "react";
import { AuthContext } from "./Containers/Auth/AuthProvider";
import { withRouter } from "react-router-dom";
import { Provider, useDispatch, useSelector } from "react-redux";
//lots of other imports
const App = (props) => {
const { location } = props;
const { user, accessTokenExpired, loading, userRole, uiConfig } = useContext(
AuthContext
);
toast.configure();
const [checkSession, setCheckSession] = useState(false);
const [modal, setModal] = useState(true);
const change_body = useSelector((state) => state.Common.change_body);
const ErrorStatusState = useSelector(
(state) => state.GetGlobalError.ErrorCode
);
const configdata = useSelector((state) => state.GetConfig.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(GetConfig());
}, []);
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.message === "Network Error") {
setCheckSession(true);
}
return Promise.reject(error);
}
);
if (loading) {
return <PageLoader />;
}
if (user === null) {
return <Login uiConfig={uiConfig} user={user} userRole={userRole} />;
}
if (userRole === null) {
return <Verification />;
}
if (ErrorStatusState > 0 || configdata.maintenance) {
return (
<>
<Provider store={store}>
<GlobalStyle />
<Header />
<ContentWrapper>
<MainContentWrapper>
<AppWrapper>
<PageWrapper>
<MainWrapper>
<ErrorClassifyRender
ErrorStatus={ErrorStatusState}
maintenance={configdata.maintenance}
/>
</MainWrapper>
</PageWrapper>
</AppWrapper>
</MainContentWrapper>
</ContentWrapper>
</Provider>
</>
);
}
return (
<Provider store={store}>
<GlobalStyle />
<Header />
<ContentWrapper>
<MainContentWrapper>
<Navigation />
<AppWrapper>
<MainApp id="appDiv">
<PageWrapper>
<MainWrapper>
<Routes />
</MainWrapper>
</PageWrapper>
</MainApp>
</AppWrapper>
</MainContentWrapper>
</ContentWrapper>
</Provider>
);
};
export default withRouter(App);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
the AuthProvider looks like:
import { useEffect, createContext, useState } from "react";
import { isAfter } from "date-fns";
import firebase from "firebase/app";
import axios from "axios";
export const AuthContext = createContext();
export const AuthProvider = ({ children, fbconfig }) => {
const baseURL = process.env.REACT_APP_BACKEND_URL || window.location.origin;
const [user, setUser] = useState(null);
const [userRole, setUserRole] = useState(null);
const [isEmailVerified, setIsEmailVerified] = useState(false);
const [loading, setLoading] = useState(true);
const [verificationLoading, setVerificationLoading] = useState(false);
const [verifyRequestBlocked, setVerifyRequestBlocked] = useState(false);
const [emailSent, setEmailSent] = useState(false);
const [isNewUser, setIsNewUser] = useState(false);
const [accessTokenExpired, setAccessTokenExpired] = useState(false);
// Configure Firebase.
const config = {
apiKey: fbconfig.FIREABSE_API_KEY,
authDomain: fbconfig.FIREABSE_AUTH_DOMAIN,
projectId: fbconfig.FIREABSE_PROJECT_ID,
};
// Configure FirebaseUI.
if (!firebase.apps.length) {
firebase.initializeApp(config);
}
const sendEmail = async ({ email, isNewUser = false }) => {
// setLoading(true);
setVerificationLoading(true);
await axios
.post(`${baseURL}/send_email_verification`, {
email,
link: window.location.origin,
})
.then((response) => {
if (response.data.error) {
setUser(response);
setEmailSent(true);
setVerifyRequestBlocked(true);
setVerificationLoading(false);
} else {
setUser(response);
setEmailSent(true);
setVerificationLoading(false);
}
})
.catch((error) => {
setVerifyRequestBlocked(true);
setUser({ data: { email: email } });
setVerificationLoading(false);
setEmailSent(true);
});
setIsNewUser(isNewUser);
};
const uiConfig = {
signInFlow: "popup",
signInOptions: [
firebase.auth.GoogleAuthProvider.PROVIDER_ID,
firebase.auth.EmailAuthProvider.PROVIDER_ID,
],
signInSuccessUrl: "/",
callbacks: {
signInSuccessWithAuthResult: async (authResult) => {
if (authResult.additionalUserInfo.isNewUser) {
await sendEmail({
isNewUser: true,
email: authResult.user.email,
});
} else {
setIsNewUser(false);
setLoading(false);
}
return false;
},
},
};
useEffect(() => {
firebase.auth().onAuthStateChanged(async (user) => {
setLoading(true);
if (user === null) {
setUser(null);
setLoading(false);
return;
}
if (user.emailVerified) {
setIsEmailVerified(user.emailVerified);
await user.getIdToken().then(async (accessToken) => {
setLoading(true);
try {
const result = await axios.post(
`${baseURL}/user`,
{
token: accessToken,
},
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
const expirationTimeInSeconds =
JSON.parse(atob(accessToken.split(".")[1])).exp * 1000;
const dateResult = isAfter(
new Date(),
new Date(expirationTimeInSeconds)
);
if (dateResult) {
setAccessTokenExpired(true);
}
localStorage.setItem("token", accessToken);
localStorage.setItem("user", JSON.stringify(result.data));
setUser({
...result.data,
token: accessToken,
status: result.status,
});
setUserRole(result.data.role);
setLoading(false);
} catch (error) {
setUser(null);
setLoading(false);
}
});
} else {
setUser({ data: user });
setLoading(false);
}
});
}, []);
return (
<AuthContext.Provider
value={{
user,
accessTokenExpired,
emailSent,
verifyRequestBlocked,
isEmailVerified,
loading,
userRole,
uiConfig,
isNewUser,
verificationLoading,
sendEmail,
}}
>
{children}
</AuthContext.Provider>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
and one of the endpoint calls are like:
import React, { useRef, useState, useEffect } from "react";
import styled from "styled-components";
import { useSelector, useDispatch } from "react-redux";
import ReactEcharts from "echarts-for-react";
import {
fetchHistory,
} from "../../Redux";
const History = (props) => {
const barRef = useRef();
const {
brand,
parentStartDate,
parentEndDate,
start_date,
end_date,
selectedId,
tabID,
showTabs,
showComponent,
detailed,
graphs,
tooltipExtra,
} = props;
const predict_data_response = useSelector((state) => state.History.predict_data);
useEffect(() => {
dispatch(
fetchHistoryroi({
brand: brand.name,
parentStartDate,
parentEndDate,
componentName,
tabID,
nestedObjects,
})
);
}, [parentStartDate, parentEndDate]);
const predict_data = ResponseFilter({ responseFilterProps });
const options = {
toolbox: getToolBox(),
textStyle: {
color: colors.white,
},
tooltip: {
formatter: (params) => {
return getTooltip({
params,
avarage,
devider,
componentName: "History",
});
},
backgroundColor: colors.cardBg,
extraCssText: extraCssText,
},
grid: {
left: "7px",
right: "7px",
bottom: "3%",
containLabel: true,
},
xAxis: [
{
axisLabel: {
fontSize: 10,
},
type: "category",
data: labels,
axisTick: {
alignWithLabel: true,
},
},
{
axisLabel: {
fontSize: 10,
},
position: "bottom",
offset: 15,
axisLine: {
show: false,
},
axisTick: {
show: false,
},
data: labelsValues,
},
],
yAxis: [
{
axisLabel: {
fontSize: 10,
formatter: (value, index) => {
return value;
},
},
splitLine: {
lineStyle: {
color: colors.lightGray,
},
},
type: "value",
},
],
series: [
{
type: "bar",
label: {
show: true,
position: ["30%", "30%"],
formatter: ({ data }) => {
const { value } = data;
return value === avarage / devider && avarage !== 0 ? "NA" : "";
},
},
barMaxWidth: 40,
data: nums,
},
],
};
return (
<ContainerWrapper>
<ReactEcharts
ref={barRef}
option={options}
data-test="HistoryGraph"/>
</ContainerWrapper>
);
};
export default History;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
this is the image on what happens when someone click a link after an hour inactiviy
any help please?
Instead of parsing the access token directly, the Firebase SDK provides User#getIdTokenResult() which provides the metadata of the token along with the token itself.
const { token, expirationTime } = await firebase.auth().currentUser.getIdTokenResult();
Instead of storing the access token in the object you are passing to setUser(), a better option would be to add a Axios request interceptor, like you are doing for the responses. This will allow you to trigger requests using Axios as you normally would, but you don't have to add { headers: { Authorization: `Bearer ${accessToken}` } } on every request.
The following useEffect() attaches an interceptor on mount and detaches it on unmount, where the interceptor attaches the user's latest ID token to the request. Here, I've also added a clause where if the token were to expire in the next 5 minutes, it's forcefully refreshed.
useEffect(() => {
const authRequestInterceptor = axios.interceptors.request.use(async (config) {
if (!config.url.startsWith(baseURL))
return config; // this request doesn't target our API, leave untouched
const currentUser = firebase.auth().currentUser;
if (currentUser === null)
return config; // no user signed in, return config unmodified
// getIdTokenResult provides token metadata in the result
// will return current token, unless it's expired where a fresh token is grabbed
let { token, expirationTime } = await currentUser.getIdTokenResult();
const expiresMS = (new Date(expirationTime)).getTime();
if (expiresMS - Date.now() < 300000) {
// if the token will expire in the next 5 minutes,
// forcefully grab a fresh token
token = await currentUser.getIdToken(true);
}
// Attach the token to the request
config.headers.Authorization = `Bearer ${token}`;
return config;
});
return () => axios.interceptors.request.eject(authRequestInterceptor);
}, []);
Note: You should use the same useEffect() strategy for your response interceptor and you should use return firebase.auth().onAuthStateChanged(...) so that the cleanup function returned by onAuthStateChanged is accessible to its useEffect().

Resources