Update state in setInterval via dispatch outside component - reactjs

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.

Related

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);
}

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

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.

Ho to wait with fetch until redux state is available?

I want to fetch some data from a database, and depending on the user the returned data should differ. The way i tried it, was by passing the userid as a query. The id is stored as a redux state. The problem is, that it takes some time before the redux state is available. Ive tried fixing this with if statements, and rerunning the useEffect everytime the auth state is updated. This doesn't work.
I want to fetch, when the redux state auth.user.id is available. Which it is like .1 sec after the initial load.
Here is my code:
const auth = useSelector((state) => state.auth);
useEffect(async () => {
if (auth.token.length > 0) {
const res = await getData(`calendar/?userId=${auth.user.id}`);
setLessons(res.lessons);
setEvents(res.events);
}
}, [auth, date]);
I believe useEffect is already asynchronous, so you don't need to use the async keyword in the anonymous callback. You can create the async function for that logic elsewhere and call it within the useEffect.
Similarly, you could put in self calling async function within your useEffect as such:
useEffect(() => {
(async () => {
if (auth.token.length) {
try {
const res = await getData(`calendar/?userId=${auth.user.id}`);
setLessons(res.lessons);
setEvents(res.events);
}catch (err) {console.log(err);}
}
})();
}, [auth, date]);
I think this link may be helpful:
React Hook Warnings for async function in useEffect: useEffect function must return a cleanup function or nothing
So with the basic understanding, I assume that you need to call the API whenever userId is available. try the below useEffect
useEffect(async () => {
// check user id is available here
if (auth.user && auth.user.id) {
const res = await getData(`calendar/?userId=${auth.user.id}`);
setLessons(res.lessons);
setEvents(res.events);
// some other statements
}
}, [auth, date]);

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
},[ ])

Redux: Why does my useEffect() keeps rerendering its value on every page rerender

I am learning react-redux.
I got the following problem:
I make two async api calls (with redux-thunk):
the first one to fetch country names (in one object, ex: {countries: [{...}, ...]}.
Those country names I use afterwards to make a second api call, to get all the soccer leagues, that are in those countrys (sometimes, there are none, so I get a null). In this case, the call is made with each countryName separatly. I make out of the results an array.
This arrays length is 255m out of which I filter out the null values and map the leagues names.
After I click on a League's name, a page is rendered ({Link} from "react-router-dom";).
NOW my problem occurs
When I click, to get back to my home page (<Link to={"/"} >), both useEffect() are making an api call again. Why?
Here is the code for my useEffect():
const dispatch = useDispatch();
const selectAllCountries = useSelector(state => state.allCountries);
const selectAllLeagues = useSelector(state => state.allLeagues);
useEffect(() => {
dispatch(allCountries());
}, [dispatch]);
useEffect(() => {
if(!_.isEmpty(selectAllCountries.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data]);
I tried to make a custom hook and put the useEffect() in there:
const useCountries = getCountries => {useEffect(() => {
dispatch(getCountries());
},[getCountries])}
useCountries(allCountries);
as suggested here:
React hooks: dispatch action from useEffect
But it didnt help.
Will be greatful for any help.
ANSWER:
in "./actions/.../allLeagues.js
...
import _ from "lodash";
export const allLeagues = (country) => async (dispatch, getState) => {
if (!_.isEmpty(getState().allLeagues) && !_.isEmpty(getState().allLeagues.data)) {
return Promise.resolve();
} else {
try {
...
}
}
}
Question, that was also helpfull:
Fetching data from store if exists or call API otherwise in React
(take look at answer about getStore())
As mentioned in a comment above, the homepage unmounts when you click to go to a new page. When you go back, the page re-mounts and the effect runs again, triggering another API call. You can prevent the API call by checking whether or not the values already exist in your store. I personally like to do this in the action creator, but you could do it in the effect as well.
Checking state in the action creator:
function allLeagues(countryName) {
return (dispatch, getState) => {
// Call `getState` and check whether `allLeagues` has been populated yet.
const { allLeagues } = getState();
if (allLeagues && allLeagues.data && allLeagues.data.length) {
// You already have the data, no need to make the API call.
return Promise.resolve();
}
// No data, make the API call...
};
}
Checking state in the effect:
useEffect(() => {
// Check whether the league data is set or not.
if(!_.isEmpty(selectAllCountries.data) && _.isEmpty(selectAllLeagues.data)) {
selectAllCountries.data.countries.map(el => dispatch(allLeagues(el.name_en)));
}
}, [dispatch, selectAllCountries.data, selectAllLeagues.data]);

Resources