LocalStorage updating inconsistently with setInterval - reactjs

After calling the refresh token endpoint to refresh the user's auth tokens, the local storage does not update the token field consistently. Sometimes, the local storage is updated properly and the app works well, other times the token and admin/student fields are deleted from local storage despite no error being logged and the endpoint returning a success response. How do I fix this? Code below
import { parseTokens, parseAdmin, parseUser } from "../utils/auth-parser";
import { adminAuthFetch } from "../config/axios/axios-admin.config";
import { studentAuthFetch } from "../config/axios/axios-user.config";
export const refresher = async () => {
const admin = parseAdmin();
const student = parseUser();
const token = parseTokens();
if (!admin && !student) {
return;
}
if (admin && !student) {
console.log(
"==========================refreshing token==================",
new Date().getMilliseconds()
);
try {
const response = await adminAuthFetch.post(`/auth/refresh-tokens`, {
refresh_token: token.refresh,
});
const data = response?.data;
console.log(data);
localStorage.setItem(
"tokens",
JSON.stringify({
access: data?.data?.auth_token,
refresh: data?.data?.refresh_token,
})
);
} catch (error) {
console.log(error);
localStorage.removeItem("tokens");
localStorage.removeItem("admin");
}
} else if (student && !admin) {
console.log(
"==========================refreshing student token==================",
new Date().getMilliseconds()
);
try {
const response = await studentAuthFetch.post(`/auth/refresh-tokens`, {
refresh_token: token.refresh,
});
const data = response?.data;
console.log(data);
localStorage.setItem(
"tokens",
JSON.stringify({
access: data?.data?.auth_token,
refresh: data?.data?.refresh_token,
})
);
} catch (error) {
console.log(error)
localStorage.removeItem("tokens");
localStorage.removeItem("student");
}
}
};
Here's the Effect that is called from the root app
const refreshFunction = () => {
if (!refreshRef.current) {
refreshRef.current = true;
refresher();
} else {
refreshRef.current = false;
}
};
useEffect(() => {
const timer = setInterval(refreshFunction, 1000 * 60 * 2);
return () => clearInterval(timer);
}, []);
Despite receiving a success response from the endpoint and ensuring refresher function is called only once with the useref check, the token field in the local storage doesn't update consistently. Sometimes the values are updated, sometimes they are deleted without an error being logged to the console. Tried removing strict mode but it still does not work

Without being certain about how everything in your code works, it's possible that despite your best intentions, the refresher function is rendering twice.
Could you share more context around the React version you're using? If you're using version 17, try doing something like this:
let log = console.log
at the top level of your code, and use it for logging instead. My working theory is that some form of console.log suppression is happening on a second render, which is why you're not getting the logs, even though the localStorage removeItem call is still executing.
Let me know the React version, and we can continue debugging.

Related

Axios throwing CanceledError with Abort controller in react

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

Reactjs Cannot Update During an Existing State Transition

I'm creating a global function that checks whether the jwt token is expired or not.
I call this function if I'm fetching data from the api to confirm the user but I'm getting the error that I cannot update during an existing state transition and I don't have a clue what it means.
I also notice the the if(Date.now() >= expiredTime) was the one whose causing the problem
const AuthConfig = () => {
const history = useHistory();
let token = JSON.parse(localStorage.getItem("user"))["token"];
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
localStorage.removeItem("user");
history.push("/login");
} else {
return {
headers: {
Authorization: `Bearer ${token}`,
},
};
}
}
};
I'm not sure if its correct but I call the function like this, since if jwt token is expired it redirect to the login page.
const config = AuthConfig()
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
I updated this peace of code and I could login to the application but when the jwt expires and it redirect to login using history.push I till get the same error. I tried using Redirect but its a little slow and I could still navigate in privateroutes before redirecting me to login
// old
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime)
// change
if (exp < Date.now() / 1000)
i would start from the beginning telling you that if this is a project that is going to production you always must put the auth token check in the backend especially if we talk about jwt authentication.
Otherwise if you have the strict necessity to put it in the React component i would suggest you to handle this with Promises doing something like this:
const config = Promise.all(AuthConfig()).then(()=> productData());
I would even consider to change the productData function to check if the data variable is not null before saving the state that is the reason why the compiler is giving you that error.
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
data && setProduct(data);
};
Finally consider putting this in the backend. Open another question if you need help on the backend too, i'll be glad to help you.
Have a nice day!
I'm still not sure how your code is used within a component context.
Currently your API and setProduct are called regardless whether AuthConfig() returns any value. During this time, you are also calling history.push(), which may be the reason why you encountered the error.
I can recommend you to check config for value before you try to call the API.
const config = AuthConfig()
if (config) {
const productData = async () => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
}
I'm assuming that AuthConfig is a hook, since it contains a hook. And that it's consumed in a React component.
Raise the responsibility of redirecting to the consumer and try to express your logic as effects of their dependencies.
const useAuthConfig = ({ onExpire }) => {
let token = JSON.parse(localStorage.getItem("user"))["token"];
const [isExpired, setIsExpired] = useState(!token);
// Only callback once, when the expired flag turns on
useEffect(() => {
if (isExpired) onExpire();
}, [isExpired]);
// Check the token every render
// This doesn't really make sense cause the expired state
// will only update when the parent happens to update (which
// is arbitrary) but w/e
if (token) {
let { exp } = jwt_decode(token);
let expiredTime = exp * 1000 - 60000;
if (Date.now() >= expiredTime) {
setIsExpired(true);
return null;
}
}
// Don't make a new reference to this object every time
const header = useMemo(() => !isExpired
? ({
headers: {
Authorization: `Bearer ${token}`,
},
})
: null, [isExpired, token]);
return header;
};
const Parent = () => {
const history = useHistory();
// Let the caller decide what to do on expiry,
// and let useAuthConfig just worry about the auth config
const config = useAuthConfig({
onExpire: () => {
localStorage.removeItem("user");
history.push("/login");
}
});
const productData = async (config) => {
const { data } = await axios.get("http://127.0.0.1:5000/product", config);
setProduct(data);
};
useEffect(() => {
if (config) {
productData(config);
}
}, [config]);
};

How to make componentWillMount to await until token is stored in local storage

I am trying to store jwt token in local storage on app startup but the render method gets called even before the token is stored in local storage. Could you please help to componentWillMount to wait until token is stored in local storage?
Index.js
componentWillMount() {
AuthRepository.getToekn();
}
AuthRepository.js
class AuthRepository {
constructor(callback) {
this.callback = callback;
}
async getToekn() {
const reponse = await axios
.post(`${baseUrl}/auth/local`, {
identifier:(process.env.IDENTIFIER).trim(),
password:(process.env.IDENTIFIER_PWD).trim(),
})
.then((response) => {
setTrapiToken(response.data.jwt)
return response.data.jwt;
})
.catch((error) => ({ error: JSON.stringify(error) }));
return reponse;
}
}
export default new AuthRepository();
Function to set token
export const setTrapiToken = (token) => {
console.log('here')
console.log(token)
try{
localStorage.setItem(process.env.JWT_KEY,token);
return true;
}catch(e){
console.log('false')
return false;
}
};
React's rendering never waits - and so mounting also never waits.
If you like it or not, that component is mounted. So all you can do is check if the data is already in place and display some kind of loading indicator if it isn't yet.

React - useEffect running even when there was no change in state variable

I have an endpoint in my kotlin app that looks like this:
either.eager<String, Unit> {
val sessionAndCookieUser = commonAuth.decryptCookieGetUser(getCookie(context), ::userTransform).bind()
val user = sessionAndCookieUser.session.user
val ctx = Ctx(ds, SystemSession, conf)
val dbUser = getUserEither(ctx, user.id).bind()
val signatureAlgorithm = SignatureAlgorithm.HS256
val signingKey = SecretKeySpec(conf.get(ZendeskJWTSecret).toByteArray(), signatureAlgorithm.jcaName)
val iat = Date(System.currentTimeMillis())
val exp = Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)
val token = Jwts.builder()
.claim("name", dbUser.name)
.claim("email", dbUser.email)
.setIssuer(conf.get(StreamAppName))
.setIssuedAt(iat)
.setExpiration(exp)
.signWith(signingKey, signatureAlgorithm)
.compact()
context.setResponseCode(StatusCode.OK)
.setResponseType("application/json")
.send(jsonObject("token" to token).toString())
}.mapLeft {
context.setResponseCode(StatusCode.UNAUTHORIZED)
}
I am setting a response where I should send a jsonObject if a user is authenticated or UNAUTHORIZED if the user is not authenticated.
When I am testing this endpoint in a browser I just get status unknown for that request - when I was debugging the backend, otherwise I get 200 with no response data.
If I test it in postman I get json as a response.
I see that token is being built and everything looks good on the backend side, but then response is not being loaded in the browser.
I am fetching it like this from react:
export const fetchGet = (uriPath: string) =>
fetch(fullUrl(uriPath), {
method: 'GET',
credentials: 'include'
})
useEffect(() => {
console.log('got here')
fetchGet('/auth/token')
.then(res => {
console.log('res ', res)
return res.json()
})
.then(res => {
console.log('res.json ', res)
return res.ok ? setJwtToken(res.token) : Promise.reject(res.statusText)
})
.catch(error => {
console.log('err ', error)
setError(error.toString())
})
}, [])
In the console I can only see 'got here' being logged, nothing else, and frontend crushed with an error:
DevTools failed to load source map: Could not load content for
data:application/json;charset=utf-8;base64, longTokenString...:
Load canceled due to reload of inspected page
What am I doing wrong here?
Updated
I found an issue here, I had 2 more useEffect functions, and they were redirecting before I had a result. I am not sure why was the useEffect function where I am passing the error state variable running when there was no change from initial state?
Here is the full code:
const [jwtToken, setJwtToken] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
fetchGet('/auth/token')
.then(async res => {
const data = await res.json()
if (!res.ok) {
const error = data?.message || res.statusText
return Promise.reject(error)
}
return data
})
.then(({token}) => setJwtToken(token))
.catch(err => {
console.log('err ', err)
setError(err.toString())
})
}, [])
useEffect(() => {
if (jwtToken) {
// window.location.href = `/mypage.com?access/jwt?jwt=${jwtToken}&return_to=`
console.log(jwtToken)
}
}, [jwtToken])
useEffect(() => {
console.log(error)
//window.location.href = '/login'
}, [error])
Update nr. 2:
const [jwtToken, setJwtToken] = useState('')
const { search } = useLocation()
useEffect(() => {
fetchGet('/auth/token')
.then(async res => {
const data = await res.json()
if (!res.ok) {
const error = data?.message || res.statusText
return Promise.reject(error)
}
return data
})
.then(({token}) => setJwtToken(token))
.catch(() => window.location.href = '/login')
}, [])
useEffect(() => {
const params = new URLSearchParams(search)
const returnTo = params.get('return_to') ? `&return_to=${params.get('return_to')}` : ''
jwtToken !== '' ? window.location.href = `${url}/jwt?jwt=${jwtToken}${returnTo}` : null
}, [jwtToken])
return <p>Authenticating ...</p>
I have removed unnecessary error useEffect function, but now I get:
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I get this warning and it is also not redirecting after the token is fetched. What am I doing wrong this time around?
Every useEffect callback will be invoked on first mount. You should include a simple if statement to ensure an error is set before running your error handling logic.
useEffect(() => {
if(error) {
console.log(error)
//window.location.href = '/login'
}
}, [error])
There is likely an issue with the CORS configuration of your API.
Access-Control-Allow-Origin response header must be set to the origin of your react app (it cannot be * for credentialed requests) and Access-Control-Allow-Credentials must be true. Failing to include them will result in an opaque response.
https://fetch.spec.whatwg.org/#cors-protocol-and-credentials
Here is my completed answer. The main problem here is using useEffect incorrectly, especially with objects in the dependency array.
Let's talk about this code first
useEffect(() => {
// TODO something with error
}, [error]);
Because error is an object and React useEffect use shallow comparison as you can see in this question. It will make the code inside that useEffect will run forever.
Next part, you get warnings because your use of redirect is not in the right way. Just remove that useEffect and it should work.
The reason why is, when we have an error, your code in your catch should run. Beside that, jwtToken will be changed at that time too. It will make your app redirected before the rendering process is completed.

Value from state not there | React

My Problem
I am using React and Apollo in my frontend application. When I login as a regular user I want to be navigated to a certain path, but before that I need to set my providers state with my selectedCompany value. However, sometimes I get my value from my provider in my components CDM and sometimes I don't, so I have to refresh in order to get the value.
I've tried to solve this, but with no luck. So I am turning to the peeps at SO.
My setup (code)
In my login component I have my login mutation, which looks like this:
login = async (username, password) => {
const { client, qoplaStore, history } = this.props;
try {
const result = await client.mutate({
mutation: LOGIN,
variables: {
credentials: {
username,
password,
}}
});
const authenticated = result.data.login;
const { token, roles } = authenticated;
sessionStorage.setItem('jwtToken', token);
sessionStorage.setItem('lastContactWithBackend', moment().unix());
qoplaStore.setSelectedUser(authenticated);
qoplaStore.setUserSessionTTL(result.data.ttlTimeoutMs);
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany();
history.push("/admin/shops");
}
} catch (error) {
this.setState({ errorMessage: loginErrorMessage(error) });
}
};
So if my login is successful I check the users roles, and in this case, I will get into the else statement. The function getAndSetSelectedCompany look like this:
getAndSetSelectedCompany = () => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0]);
});
};
So I am fetching my companies try to set one of them in my providers state with the function setSelectedCompany. selectedValues is what im passing down from my consumer to all my routes in my router file:
<QoplaConsumer>
{(selectedValues) => {
return (
...
)
}}
</QoplaConsumer
And in my provider I have my setSelectedCompany function which looks like this:
setSelectedCompany = company => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
})
};
And my selectedValues are coming from my providers state.
And in the component that has the route I'm sending the user to I have this in it's CDM:
async componentDidMount() {
const { client, selectedValues: { selectedCompany, authenticatedUser } } = this.props;
console.log('authenticatedUser', authenticatedUser)
if (selectedCompany.id === null) {
console.log('NULL')
}
Sometimes I get into the if statement, and sometimes I don't. But I rather always come into that if statement. And that is my current problem
All the help I can get is greatly appreciated and if more info is needed. Just let me know.
Thanks for reading.
Your getAndSetSelectedCompany is asynchronous and it also calls another method that does setState which is also async.
One way to do this would be to pass a callback to the getAndSetSelectedCompany that would be passed down and executed when the state is actually set.
changes to your login component
if (roles.includes(ROLE_SUPER_ADMIN)) {
history.push("/admin/companies");
} else {
// This is where I'm at
this.getAndSetSelectedCompany(()=>history.push("/admin/shops"));
}
changes to the two methods that are called
getAndSetSelectedCompany = (callback) => {
const { client, selectedValues, qoplaStore } = this.props;
client.query({ query: GET_COMPANIES }).then(company => {
selectedValues.setSelectedCompany(company.data.getCompanies[0], callback);
});
};
setSelectedCompany = (company, callback) => {
this.persistToStorage(persistContants.SELECTED_COMPANY, company);
this.setState({
selectedCompany: company
}, callback)
};

Resources