I'm trying to understand the state in React, having a background with Vue.js.
I'm building a login which fetches a JWT-token, then stores the token in a global store so we can use it for subsequent API-calls. I'm using an axios interceptor to resolve the token from the store. However, the token is always an old version/from previous render.
I've read about the React lifecycle but this function is not used in the rendering. It's used as a callback. I understand that setting the state is async. I've tried wrapping the interceptor in a useEffect(.., [tokenStore.token]) and using setTimeout(). I feel like I'm missing something.
Why is my state not being updated in my callbacks? Am I going about this in an non-idiomatic way?
Usage:
<button onPress={() => loginWithToken('abc')}>
Sign In
</button>
User hook:
export function useUserState() {
const api = useApi();
function loginWithToken(token) {
tokenState.setToken(token);
api
.request('get', 'currentUser')
.then((data) => {
console.log(data);
})
.catch((errors) => {
console.log(errors);
});
}
}
The api:
export default function useApi(hasFiles = false) {
const tokenState = useTokenState();
const client = axios.create(/*...*/);
client.interceptors.request.use(function (config) {
config.headers!.Authorization = tokenState.token
? `Bearer ${tokenState.token}`
: '';
return config;
});
// ...
}
Token store using hookstate:
const tokenState = createState({
token: null,
});
export function useTokenState() {
const state = useState(tokenState);
return {
token: state.token.get(),
setToken: (token: string | null) => {
console.log('setToken: ' + token);
state.set({ token });
},
};
}
I solved it by updating the defaults instead:
setToken: token => {
apiClient.defaults.headers.Authorization = token
? `Bearer ${token}`
: undefined;
// ...
Related
I have built an axios private instance with interceptors to manage auth request.
The system has a custom axios instance:
const BASE_URL = 'http://localhost:8000';
export const axiosPrivate = axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
A custom useRefreshToken hook returns accessToken using the refresh token:
const useRefreshToken = () => {
const { setAuth } = useAuth();
const refresh = async () => {
const response = await refreshTokens();
// console.log('response', response);
const { user, roles, accessToken } = response.data;
setAuth({ user, roles, accessToken });
// return accessToken for use in axiosClient
return accessToken;
};
return refresh;
};
export default useRefreshToken;
Axios interceptors are attached to this axios instance in useAxiosPrivate.js file to attached accessToken to request and refresh the accessToken using a refresh token if expired.
const useAxiosPrivate = () => {
const { auth } = useAuth();
const refresh = useRefreshToken();
useEffect(() => {
const requestIntercept = axiosPrivate.interceptors.request.use(
(config) => {
// attach the access token to the request if missing
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `Bearer ${auth?.accessToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
const responseIntercept = axiosPrivate.interceptors.response.use(
(response) => response,
async (error) => {
const prevRequest = error?.config;
// sent = custom property, after 1st request - sent = true, so no looping requests
if (error?.response?.status === 403 && !prevRequest?.sent) {
prevRequest.sent = true;
const newAccessToken = await refresh();
prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
return axiosPrivate(prevRequest);
}
return Promise.reject(error);
}
);
// remove the interceptor when the component unmounts
return () => {
axiosPrivate.interceptors.response.eject(responseIntercept);
axiosPrivate.interceptors.request.eject(requestIntercept);
};
}, [auth, refresh]);
return axiosPrivate;
};
export default useAxiosPrivate;
Now, this private axios instance is called in functional component - PanelLayout which is used to wrap around the pages and provide layout.
Here, I've tried to use AbortControllers in axios to terminate the request after the component is mounted.
function PanelLayout({ children, title }) {
const [user, setUser] = useState(null);
const axiosPrivate = useAxiosPrivate();
const router = useRouter();
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
getUserProfile();
return () => {
isMounted = false;
controller.abort();
};
}, []);
console.log('page rendered');
return (
<div className='flex items-start'>
<Sidebar className='h-screen w-[10rem]' />
<section className='min-h-screen flex flex-col'>
<PanelHeader title={title} classname='left-[10rem] h-[3.5rem]' />
<main className='mt-[3.5rem] flex-1'>{children}</main>
</section>
</div>
);
}
export default PanelLayout;
However, the above code is throwing the following error:
CanceledError {message: 'canceled', name: 'CanceledError', code: 'ERR_CANCELED'}
code: "ERR_CANCELED"
message: "canceled"
name: "CanceledError"
[[Prototype]]: AxiosError
constructor: ƒ CanceledError(message)
__CANCEL__: true
[[Prototype]]: Error
Please suggest how to avoid the above error and get axios to work properly.
I also encountered the same issue and I thought that there was some flaw in my logic which caused the component to be mounted twice. After doing some digging I found that react apparently added this feature with with the new version 18 in StrictMode where useEffect was being run twice. Here's a link to the article clearly explaining this new behaviour.
One way you could solve this problem is by removing StrictMode from your application (Temporary Solution)
Another way is by using useRef hook to store some piece of state which is updated when your application is mounted the second time.
// CODE BEFORE USE EFFECT
const effectRun = useRef(false);
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const signal = controller.signal;
const getUserProfile = async () => {
try {
const response = await axiosPrivate.get('/api/identity/profile', {
signal,
});
console.log(response.data);
isMounted && setUser(response.data.user);
} catch (error) {
console.log(error);
router.push({
pathname: '/seller/auth/login',
query: { from: router.pathname },
});
}
};
// Check if useEffect has run the first time
if (effectRun.current) {
getUserProfile();
}
return () => {
isMounted = false;
controller.abort();
effectRun.current = true; // update the value of effectRun to true
};
}, []);
// CODE AFTER USE EFFECT
Found the solution from this YouTube video.
I, too, encountered this issue. What made it worse is that axios doesn't provide an HTTP status code when the request has been canceled, although you do get error.code === "ERR_CANCELED". I solved it by handling the abort within the axios interceptor:
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
if (error.code === "ERR_CANCELED") {
// aborted in useEffect cleanup
return Promise.resolve({status: 499})
}
return Promise.reject((error.response && error.response.data) || 'Error')
}
);
As you can see, I ensure that the error response in the case of an abort supplies a status code of 499.
I faced the same problem in similar project, lets start by understanding first the root cause of that problem.
in react 18 the try to make us convenient to the idea of mounting and unmounting components twice for future features that the are preparing, the the useEffect hook now is mounted first time then unmounted the mounted finally.
so they need from us adapt our projects to the idea of mount and unmount of components twice
so you have two ways, adapting these changes and try to adapt your code to accept mounting twice, or making some turn around code to overcome mounting twice, and I would prefer the first one.
here in your code after first mount you aborted your API request in clean up function, so when the component dismount and remount again it face an error when try to run previously aborted request, so it throw exception, that's what happens
1st solution (adapting to react changing):
return () => {
isMounted = false
isMounted && controller.abort()
}
so in above code we will abort controller once only when isMounted is true, and thats will solve your problem
2nd solution (turn around to react changing):
by using useRef hook and asign it to a variable and update its boolean value after excuting the whole code only one time.
const runOnce = useRef(true)
useEffect(()=>{
if(runOnce.current){
//requesting from API
return()=>{
runOnce.current = false
}
}
},[])
3rd solution (turn around to react changing):
remove React.StrictMode from index.js file
enter image description here
Here i have screen shot of my local storage. how can i fetch access token from there pass as headers in below action page. please provide any solution for this. how we can fetch token from local storage using react redux and display in action page.
import axios from 'axios';
export const upiAction = {
upi,
};
function upi(user) {
return (dispatch) => {
var data = {
upiId: user.upiId,
accountNumber: user.accountNumber,
};
axios
.post('http://localhost:9091/upiidcreation', data,
)
.then((res) => {
console.log("res", (res));
const { data } = res;
alert(JSON.stringify(data.responseDesc));
// window.location.pathname = "./homes";
if (data.responseCode === "00") {
window.location.pathname = "./home"
}
})
.catch(err => {
dispatch(setUserUpiError(err, true));
alert("Please Check With details");
});
};
}
export function setUserUpi(showError) {
return {
type: 'SET_UPI_SUCCESS',
showError: showError,
};
}
export function setUserUpiError(error, showError) {
return {
type: 'SET_UPI_ERROR',
error: error,
showError: showError,
};
}
if you just need to fetch the token and send it as header in the api request you can do this
let storageValue =JSON.parse(localStorage.getItem('currentUser')
storageValue object will have the whole thing that you've stored in localStorage .
axios.post('http://localhost:9091/upiidcreation', data, {
headers: {
token : storageValue?.data?.accessToken
}
})
You can get localStorage Object like this
let localStorageObject = JSON.parse(localStorage.getItem('currentUser'));
Then You can use it that object to get access token like this:
localStorageObject?.data?.accessToken
I am trying to attach a JWT token from AWS Cognito to Uppy requests in my upload component. To get the token, I believe I need an async function:
async function getSessionToken() {
const data = (await Auth.currentSession()).getAccessToken().getJwtToken()
console.log(data)
return data;
}
Then I use this return value in the actual function component:
export default function UppyUpload () {
const data = getSessionToken();
const uppy = useUppy(() => {
return new Uppy({
debug: true,
autoProceed: false,
restrictions: {
maxNumberOfFiles: 1,
minNumberOfFiles: 1,
allowedFileTypes: ['video/*'],
requiredMetaFields: ['caption'],
}
})
.use(AwsS3Multipart, {
limit: 4,
companionUrl: 'http://localhost:3020/',
companionHeaders: {
'Authorization': "Bearer " + data,
'uppy-auth-token': "Bearer " + data,
}
})
...
However, data inside UppyUpload returns a promise, as anticipated. But I need this to resolve into a value somehow because I think the Uppy initialization requires this value (Authorization': "Bearer " + data) at the time of function rendering.
I'm not sure how to resolve this issue, but I feel like this is probably a common problem. Is there a recommended way?
await should be used outside the function
async function getSessionToken() {
const data = await (Auth.currentSession()).getAccessToken().getJwtToken()
console.log(data)
return data;
}
I try to use social login.
if I success to login in kakao. they give me access_token and I use it to mutation to my server
below is my code
import { useMutation } from "react-apollo-hooks";
import { KAKAO_LOGIN } from "./AuthQuery";
export default () => {
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: { provder: "kakao", accessToken: authObj.access_token },
});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function (authObj) {
console.log(authObj.access_token);
},
});
};
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
return (
<a href="#" onClick={kakaoLogin}>
<h1>카카오로그인</h1>
</a>
);
};
if I success to login using by function kakaoLogin, it give authObj.
console.log(authObj.access_token) show me access_token
and I want to use it to useMutation. but it show to me authObj is not defined.
Looks like you're looking for a local state to hold authObj
const [authObj, setAuthObj] = useState({});
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
setAuthObj(authObj);
},
});
};
const kakaoLoginMutation = useMutation(KAKAO_LOGIN, {
variables: {
provder: "kakao",
accessToken: authObj.access_token
},
});
if (authObj.access_token !== "") {
kakaoLoginMutation();
}
After reading Apollo auth docs (you read it, right?) you should know tokens should be sent using headers.
... if auth link is used and reads the token from localStorage ... then any login function (mutation, request) result should end storing token in localStorage (to be passed by header in next queries/mutations) ... it should be obvious.
In this case
... we have a bit different situation - kakao token is passed into login mutation as variable ...
We can simply, directly (not using state, effect, rerenderings) pass the kakao token to 'mutation caller' (kakaoLoginMutation):
// mutation definition
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN);
// event handler
const kakaoLogin = (e) => {
e.preventDefault();
window.Kakao.Auth.login({
success: function(authObj) {
// run mutation
kakaoLoginMutation({
variables: {
provder: "kakao",
accessToken: authObj.access_token
}
})
},
});
};
When required, login mutation (KAKAO_LOGIN) result can be processed within onCompleted handler:
const [kakaoLoginMutation] = useMutation(KAKAO_LOGIN,
onCompleted = (data) => {
// save mutation result in localStorage
// set some state to force rerendering
// or redirection
}
);
Last few days I tried to write some middleware that checks wether the token stored in the redux-store is still valid and not reached it's expiry date. If it is not valid anymore it should refresh the token before executing any other async call. The problem I am encountering right now is that the async redux functions in the components are called first before the middleware is being called.
Currently I wrote the following middleware:
reduxMiddleware.js
const refreshJwt = ({ dispatch, getState }) => {
return (next) => (action) => {
console.log(typeof action);
if (typeof action === "function") {
if (getState().authentication.token) {
// decode jwt so that we know if and when it expires
var tokenExpiration = parseJwt(getState().authentication.token).exp;
if (
tokenExpiration &&
moment(tokenExpiration) <
moment(Math.floor(Date.now().valueOf() / 1000))._i
) {
console.log("start refreshing");
startRefreshToken(getState().authentication.refreshToken).then(
(token) => {
console.log("done refreshing");
dispatch(updateAccessToken(token));
next(action);
}
);
}
}
}
return next(action);
};
};
export default refreshJwt;
I apply this middleware like so:
export default () => {
const store = createStore(
combineReducers({
authentication: authenticationReducer,
venue: venueReducer,
tables: tableReducer
}),
composeEnhancers(applyMiddleware(refreshJwt, thunk))
);
return store;
};
The startRefreshToken code is:
const startRefreshToken = (refresh_token) => {
return httpPost(
process.env.NODE_ENV
? `https://tabbs-api.herokuapp.com/api/v1/token`
: `http://localhost:3000/api/v1/token`,
{
refresh_token
}
)
.then((response) => {
localStorage.setItem(
"authentication",
JSON.stringify({
token: response.data.token,
refreshToken: refresh_token
})
);
return response.data.token;
})
.catch((error) => {
return Promise.reject(error.response);
});
};
Order of calling:
Legend:
Executing call now stands for the function being called in the component
start refreshing stands for the middleware being called
Currently I am experiencing the following issue:
When a async function in the components didComponentMount is being called, it will be called before the middleware function is being called. This is causing that it will be using the old token stored in the redux/local storage.
I really can't find the issue till today and would like to get some external help for this issue.
I am aware that this is duplicate of :
How to use Redux to refresh JWT token?
Thanks for the help. If you'll need additional context / code please do not hesitate to comment. I'll add it to codesandbox.
Best Kevin.