How to save and then later delete timeoutId in React inside a async thunk that is dispatched - reactjs

I have a problem that I don't know how to fix.
I want to call refresh-token endpoint when setTimeout time runs out. setTimeout is called in 2 different ways:
When user logs in
When user enters a page again and has bearer token
If I just called setTimeout without deleting the previous one, a user could log in and out and log in again and it would send a request to the refresh-token endpoint for every setTimeout that was created (in this case, 2 requests).
What I want to do is save the setTimeout that was created and if user decides to log out and then log in again, we delete setTimeout and create a new one. However, when I store my timeoutId with useState, I get Invalid hook call error.
This is how my code looks like:
storageActions.ts
export function setRefreshTokenTimeout(bearerToken: string, refreshToken: string) {
removeRefreshTokenTimeout();
var decodedBearerToken: any = jwt_decode(bearerToken);
let remainingDuration = calculateRemainingTime(decodedBearerToken.exp * 1000);
remainingDuration = 10000;
const refreshTokens = setTimeout(() => store.dispatch(refreshTokensThunk({ refreshToken })), remainingDuration);
setRefreshTokensTimeout(refreshTokens);
}
export function removeRefreshTokenTimeout() {
if (refreshTokensTimeout) {
clearTimeout(refreshTokensTimeout);
setRefreshTokensTimeout(undefined);
}
}
authActions.ts
export const userLoginThunk = createAsyncThunk<UserLoginResponse, UserLoginRequest>(
"auth/user-login",
async (request, thunkAPI) => {
let response = await AuthServiceUserLogin(request);
setUserLocalStorageData(response.data);
setRefreshTokenTimeout(response.data.bearerToken, response.data.refreshToken);
return response.data;
}
);
export const authUserThunk = createAsyncThunk("auth/authUser", async thunkAPI => {
await AuthServiceAuthUser();
const userStorage = getUserLocalStorageData();
if (userStorage?.data?.bearerToken) {
setRefreshTokenTimeout(userStorage.data.bearerToken, userStorage.data.refreshToken);
}
});
All of these are called inside a createAsyncThunk that is dispatched. Is there any way to solve this problem?

I knew this was because of useState hook, but I thought that you can only save states with hooks like useState or useRef.
I just figured out that this rule only refers to React components. storageActions.ts isn't a component, therefore I can use a simple variable to save my timeoutId.
I tested it and it works.

Related

Supabase onAuthStateChange() triggers when switching tabs in React

I have the following code in my React project using Supabase:
// supabaseClient.ts
export const onAuthStateChangedListener = (callback) => {
supabase.auth.onAuthStateChange(callback);
};
// inside user context
useEffect(() => {
const unsubscribe = onAuthStateChangedListener((event, session) => {
console.log(event);
});
return unsubscribe;
}, []);
However, every time I switch tabs away from the tab rendering the website to something else, and back, I see a new log from this listener, even if literally no change happened on the website.
Does anyone know the reason for this? The useEffect inside my user context component is the only place in my app where the listener is being called. To test, I wrote this dummy function inside my supabaseClient.ts file:
const testFunction = async () => {
supabase.auth.onAuthStateChange(() => {
console.log("auth state has changed");
});
};
testFunction()
This function also renders every time I switch tabs. This makes it a little annoying because my components that are related to userContext re render every time a tab is switched, so if a user is trying to update their profile data or something, they cannot switch tabs away in the middle of editing their data.
Supabase onAuthStateChange by default triggers every time a tab is switched. To prevent this, when initializing the client, add {multiTab: false} as a parameter.
Example:
const supabase = createClient(supabaseUrl, supabaseAnonKey, {multiTab: false,});
Here is my solution to the same problem. The way I've found is saving the access token value in a cookie every time the session changes, and retrieve it when onAuthStateChange get triggered, so I can decide to not update anything if the session access token is the same.
// >> Subscribe to auth state changes
useEffect(() => {
let subscription: Subscription
async function run() {
subscription = Supabase.auth.onAuthStateChange(async (event, newSession) => {
// get current token from manually saved cookie every time session changes
const currentAccessToken = await getCurrentAccessToken()
if (currentAccessToken != newSession?.access_token) {
console.log('<<< SUPABASE SESSION CHANGED >>>')
authStateChanged(event, newSession)
} else {
console.log('<<< SUPABASE SESSION NOT CHANGED >>>')
}
}).data.subscription
// ** Get the user's session on load
await me()
}
run()
return function cleanup() {
// will be called when the component unmounts
if (subscription) subscription.unsubscribe()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

ReactJS delay update in useState from axios response

I am new to react js and I am having a hard time figuring out how to prevent delay updating of use state from axios response
Here's my code:
First, I declared countUsername as useState
const [countUsername, setUsername] = useState(0);
Second, I created arrow function checking if the username is still available
const checkUser = () => {
RestaurantDataService.checkUsername(user.username)
.then(response => {
setUsername(response.data.length);
})
.catch(e => {
console.log(e);
})
}
So, every time I check the value of countUsername, it has delay like if I trigger the button and run checkUser(), the latest response.data.length won't save.
Scenario if I console.log() countUseranme
I entered username1(not available), the value of countUsername is still 0 because it has default value of 0 then when I trigger the function once again, then that will just be the time that the value will be replaced.
const saveUser = () => {
checkUser();
console.log(countUsername);
}
Is there anything that I have forgot to consider? Thank you
usually there is a delay for every api call, so for that you can consider an state like below:
const [loading,toggleLoading] = useState(false)
beside that you can change arrow function to be async like below:
const checking = async ()=>{
toggleLoading(true);
const res = await RestaurantDataService.checkUsername(user.username);
setUsername(response.data.length);
toggleLoading(false);
}
in the above function you can toggle loading state for spceifing checking state and disable button during that or shwoing spinner in it:
<button onClick={checking } disabled={loading}>Go
i hope this help
.then is not synchronous, it's more of a callback and will get called later when the api finishes. So your console log actually goes first most of the time before the state actually saves. That's not really something you control.
You can do an async / await and return the data if you need to use it right away before the state changes. And I believe the way state works is that it happens after the execution:
"State Updates May Be Asynchronous" so you can't really control when to use it because you can't make it wait.
In my experience you use the data right away from the service and update the state or create a useEffect, i.g., useEffect(() => {}, [user]), to update the page with state.
const checkUser = async () => {
try {
return await RestaurantDataService.checkUsername(user.username);
} catch(e) {
console.log(e);
}
}
const saveUser = async () => {
const user = await checkUser();
// do whatever you want with user
console.log(user);
}

Update state in setInterval via dispatch outside component

I currently have a functional component Form that triggers a task to occur. Once the submission is complete, I create a setInterval poll to poll for the status of the task. The code roughly looks like
export function Form(props: FormProps) {
const dispatch = useDispatch()
const pollTaskStatus = () => {
const intervalId = setInterval(async() => {
const response = await fetchTaskStatus() // Function in different file
if (response.status === 'COMPLETE') {
dispatch(Actions.displayTaskComplete())
clearInterval(intervalId)
}
})
}
const submitForm = async() => {
await onSubmitForm() // Function in different file
pollTaskStatus()
}
return (
...
<button onClick={submitForm}>Submit</button>
)
}
When the action is dispatched, the redux store is supposed to be updated and a component is supposed to update alongside it showing a message that the task is complete. However, I see the action logged with an updated store state but nothing occurs. If I just try to dispatch the same action with useEffect() wrapped around it outside the submitForm functions, the message appears. I've searched online and people say that you need to wrap useEffect around setInterval but I can't do that because the function that calls setInterval is not a custom hook or component. Is there a way to do this?
It's a bit difficult to answer your question without seeing all the code.
But my guts feeling is that this might no have anything to do with React.
const pollTaskStatus = () => {
const intervalId = setInterval(async() => {
console.log('fire to fetch')
const response = await fetchTaskStatus() // Function in different file
if (response.status === 'COMPLETE') {
console.log('success from fetch')
dispatch(Actions.displayTaskComplete())
}
})
}
Let's add two console lines to your code. What we want to see is to answer the following questions.
is the setInterval called in every 500ms?
is any of the call finished as success?
how many dispatch has been fired after submission
If you can answer all these questions, most likely you can figure out what went wrong.

How to clean up redux state with redux thunk when unmounting component before finishing fetch in useEffect?

After some time working with react-redux and redux thunk I have realise about a behaviour, which isnt the best user experience.
I know that when you are working with react and you are fetching data in useEffect when the component is rendering and for any reason you go back or navigate somewhere else you need to clear the state with a function in the return (which will recreate the componentWillUnmount lifecycle)
This problem I am facing however occurs when working with redux thunk because the data fetch is with the actions creators. So to make my long story short I will show some code. The fetching action looks something like this:
export const fetchData = () => async (dispatch, getState) => {
try {
dispatch(fetchDataStart())
const response = await fetch('https://jsonplaceholder.typicode.com/photos')
dispatch(fetchDataSucess(data))
} catch (error) {
console.log(error)
}
}
Let's say that I call this action in my component useEffect like this:
useEffect(() => {
const loadData = async () => {
await dispatch(fetchData())
}
loadData()
return () => dispatch(resetData())
},[ ])
As you can see I am dispatching a resetData action to clear the state when the component unmounts BUUUUT this is where the problem arrives. If before fetching the data the user navigates to another page, the resetData will be dispatched BUT as the fetch could not be finished the data will be stored after having been reseted. So when the user navigates back to that component it will blink (show only very quickly, maybe for a second) the old data before loading the new one. So is there any way to avoid this problem with redux thunk?
PD: I could block the navigation or the whole screen with a backdrop so the user wont navigate until the fetch is finished but i feel like that is kind of a workaround of the problem. However, let me know if you think that this would still be the best way.
Thank you.
You can create an AbortController instance. That instance has a signal property, and we pass the signal as a fetch option. Then to cancel data fetching we call the AbortController's abort property to cancel all fetches that use that signal.
export const fetchData = () => async (dispatch) => {
try {
const controller = new AbortController();
const { signal } = controller;
dispatch(fetchDataStart(controller)); // save it to state to call it later
const response = await fetch('https://jsonplaceholder.typicode.com/photos', { signal });
dispatch(fetchDataSucess(data));
} catch (error) {
console.log(error);
}
}
useEffect:
useEffect(() => {
dispatch(fetchData());
return () => dispatch(resetData()) // resetData will call controller.abort() that was saved in state
},[ ])

React onClick delete dispatch won't send second dispatch request after response received

In a component I have a button that onClick dispatches a deleteQuestion action that sends a fetch backend delete request, and when the response is received is supposed to call another action to update the Redux store.
However, since it's an onClick event, the deleteQuestion thunk function does not work like a traditional dispatch request made from ComponentWillMount and instead returns an anonymous function with a dispatch parameter that never is called. Therefore, I'm required to call the dispatch twice simultaneously in the onClick method like so:
handleDelete = () => {
const { questionId } = this.props.match.params
const { history } = this.props
deleteQuestion(questionId, history)(deleteQuestion); //calling method twice
}
While this approach is effective for trigging the delete request to the Rails backend, when I receive the response, the second dispatch function that I have embedded in the deleteQuestion action -- dispatch(removeQuestion(questionId)) -- won't trigger to update the Redux store. I've tried placing multiple debuggers in the store and checking console and terminal for errors, but nothing occurs.
I've read through the Redux docs and other resources online and from what I've been able to find they all say it should be possible to include a second dispatch call in a .then request. While it's possible to do this in get, post, and patch requests, I can't figure out why it won't work in a delete request.
The thunk call I make is:
export function deleteQuestion(questionId, routerHistory) {
return (dispatch) => {
fetch(`${API_URL}/questions/${questionId}`, {
method: 'DELETE',
}).then(res => {
dispatch(removeQuestion(questionId))
})
}
}
And the github is:
https://github.com/jwolfe890/react_project1/blob/master/stumped-app-client/src/actions/questions.js
I'd really appreciate any insight, as I've been trying to get passed this for two days now!
You are calling the action deleteQuestion directly instead of having your store dispatch the delete question action for you. You should instead call the deleteQuestion from your props that is already mapped to dispatch:
handleDelete = () => {
const { questionId } = this.props.match.params
const { history } = this.props
this.props.deleteQuestion(questionId, history);
}
If you pass in an object as mapDispatchToProps each element is dispatch call. In other words your mapDispatchToProps is equivalent to:
(dispatch) => ({
deleteQuestion: (...params) => dispatch(deleteQuestion(...params))
})

Resources