Stop setInterval function set inside useEffect - javascript

I have this simple useEffect code. When the user logged in to the application every 2 minutes I will dispatch an action which is an API call, and I need to stop this interval once a user is logged out. Still, the current code even runs after the user is logged out, what shall I do to prevent this interval when the user logs out.
I am using the value from the localStorage to determine whether the user is logged in or not.
const intervalId = useRef(null)
useEffect(() => {
const isLoggedIn = localStorage.getItem("isUserLoggedIn") //(true or false)
intervalId.current = setInterval(() => {
dispatch(refreshUserToken());
if(isLoggedIn === false){
clearInterval(intervalId.current)
}
},1000*60*2)
return () => {
clearInterval(intervalId.current)
}
},[])
Is there any way to resolve my issue?
Any help would be much appreciated!!

You should be adding the line where you get that value from localStorage inside the interval, if you want the updated value. Also, localStorage would gives you a string instead of a boolean, either you parse it, or you change your if statement. Try with this:
const intervalId = useRef(null);
useEffect(() => {
intervalId.current = setInterval(() => {
const isLoggedIn = localStorage.getItem("isUserLoggedIn"); //(true or false)
if (isLoggedIn === "false") {
clearInterval(intervalId.current);
return;
}
dispatch(refreshUserToken());
}, 1000 * 60 * 2);
return () => {
clearInterval(intervalId.current);
};
}, []);
You could use an event instead of a setInterval. As an example, change the code where you are setting the localStorage to this:
localStorage.setItem("isUserLoggedIn", true); // or false depending on the context
window.dispatchEvent(new Event("storage")); // you notice that there is a change
You change your useEffect to this:
useEffect(()=>{
const listenStorageChange = () => {
const isLoggedIn = localStorage.getItem("isUserLoggedIn");
console.log(isLoggedIn);
// do your logic here
};
window.addEventListener("storage", listenStorageChange);
return () => window.removeEventListener("storage", listenStorageChange);
},[])

The keys and the values stored with localStorage are always in the UTF-16 string format. As with objects, integer keys and booleans are automatically converted to strings.
So you have to call like this:
if(isLoggedIn === 'false'){
clearInterval(intervalId.current)
}
Check the documentation.

Related

can't use state value right after setState()

Currently i'm doing a quiz composed by multiple categories that can be chosen by the user and i wanna check if the user responded to all questions. For doing that, i compared the number of questions he answered with the number of questions gived by the api response. The problem is that i have an "submit answers" button at the end of the last question, with that onClick function:
const sendAnswers = (e, currentQuiz) => {
setQuizzes({...quizzes, [currentQuiz]:answers});
setAnswers([])
var answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in quizzes){
if(Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
answeredToAllQuestions=false;
}
}
});
if(answeredToAllQuestions === false){
setAlertTrigger(1);
}
else{
setNumber(number+1);
}
}
in that function i use setState on this line: setQuizzes({...quizzes, [currentQuiz]:answers}); to upload the answers he checked on the last question before checking if he answered to all questions. The problem is that state of quizzes is not updated imediatly and it s not seen by the if condition.
I really don't know how am i supposed to update the state right after setting it because, as i know, react useState updates the state at the next re-render and that causes trouble to me..
Considering that quizzes will be equal to {...quizzes, [currentQuiz]:answers} (after setQuizzes will set it), there is no reason to use quizzes in if condition. Replace it with a local var and problem will be solved.
const sendAnswers = (e, currentQuiz) => {
let futureValueOfQuizzes = {...quizzes, [currentQuiz]:answers}
setQuizzes(futureValueOfQuizzes);
setAnswers([])
var answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in futureValueOfQuizzes){
if(Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
answeredToAllQuestions=false;
}
}
});
if(answeredToAllQuestions === false){
setAlertTrigger(1);
}
else{
setNumber(number+1);
}
}
I would like to take this opportunity to say that these type of problems appear when you use React state for your BI logic. Don't do that! Much better use a local var defined in components body:
const Component = () => {
const [myVar , setMyVar] = useState();
let myVar = 0;
...
}
If myVar is used only for BI logic, use the second initialization, never the first!
Of course sometimes you need a var that is in BI logic and in render (so the state is the only way). In that case set the state properly but for script logic use a local var.
You have to either combine the useState hook with the useEffect or update your sendAnswers method to perform your control flow through an intermediary variable:
Using a temporary variable where next state is stored:
const sendAnswers = (e, currentQuiz) => {
const newQuizzes = {...quizzes, [currentQuiz]:answers};
let answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in newQuizzes){
if (Object.keys(quiz.questions).length !== Object.keys(newQuizzes[quiz.category]).length){
answeredToAllQuestions = false;
}
}
});
setQuizzes(newQuizzes);
setAnswers([]);
if (answeredToAllQuestions === false) {
setAlertTrigger(1);
} else {
setNumber(number+1);
}
}
Using the useEffect hook:
const sendAnswers = (e, currentQuiz) => {
setQuizzes({...quizzes, [currentQuiz]:answers});
setAnswers([]);
}
useEffect(() => {
let answeredToAllQuestions = true
DataState.map(function (quiz) {
if(quiz.category in quizzes){
if (Object.keys(quiz.questions).length !== Object.keys(quizzes[quiz.category]).length){
answeredToAllQuestions = false;
}
}
});
if (answeredToAllQuestions === false) {
setAlertTrigger(1);
} else {
setNumber(number+1);
}
}, [quizzes]);

REACT JS reset integer counter

I have this function that gets data from a service using a fetch api call and waits for the response using async and await. If the response isn't null, it loads a react component and passes the fetched data to the component, it uses react state to manage data content.
Because of the wait, i had to introduce an integer counter to help me manage the react page. So the integer counter is initialized to 0 and only increments if the response from fetch call isn't null. So i keep showing a progress bar as long as the counter is 0. Once the data state changes, the integer counter is incremented and the page loads the a new react component with the fetched data.
function CheckDeliveries(){
const [deliveries, setDeliveries] = useState(null);
const urlAPpend = "delivery/findByCustomerId/";
let userid = JSON.parse(localStorage.getItem("User"))["userId"];
const httpMethod = 'GET';
let token = localStorage.getItem("Token");
console.error('RELOAD >>>>>>', reload);
if(reload < 1){
makeApiAuthCallsWithVariable(userid,urlAPpend,httpMethod,token).then(
data => {
if (data !== null) {
//console.log("Api response: Data "+JSON.stringify(data));
setDeliveries(data);
reload++
}else{
console.error('Response not ok', data);
}
}
)
}
if(reload >= 1 && deliveries !== null){
reload = 0;
console.error('RELOAD AllDeliveryDiv >>>>>>', reload);
return (<AllDeliveryDiv obj = {deliveries} />);
}else if(reload >= 1 && deliveries === null){
reload = 0;
console.error('RELOAD MakeDeliveryDiv >>>>>>', reload);
return (<MakeDeliveryDiv />);
}else if(reload === 0){
return ( <ProgressBar striped variant="primary" now={value}/>);
}
}
My Question
I have tried using a boolean state instead of integer counter but the page gets into an infinite loop whenever i update the boolean state. Is there a way i can implement this boolean state in without experiencing the infinite loop ?
After i fetch the data, and reset the counter to 0. I log the value before reset and after reset and i get 1 and 0 respectively. But when i call this function again without reloading the entire page, counter value remains 1 even after i had reset it earlier. How do i resolve this?
Any better way to implement this, please share.
It's hard to really tell what you're going for with reload, so that's why I left the whole MakeDeliveryDiv stuff out, but this would load data from your endpoint when the component is first mounted using the useEffect side effect hook:
function CheckDeliveries() {
const [deliveries, setDeliveries] = useState(null);
const [loaded, setLoaded] = useState(false);
React.useEffect(() => {
const userid = JSON.parse(localStorage.getItem("User"))["userId"];
const token = localStorage.getItem("Token");
makeApiAuthCallsWithVariable(
userid,
"delivery/findByCustomerId/",
"GET",
token,
).then((data) => {
setDeliveries(data);
setLoaded(true);
});
}, []);
if (!loaded) {
return <ProgressBar striped variant="primary" now={value} />;
}
if (deliveries === null) {
return <>Data not OK.</>;
}
return <AllDeliveryDiv obj={deliveries} />;
}

React setTimeout with Loop

I am pulling documents from Firebase, running calculations on them and separating the results into an array. I have an event listener in place to update the array with new data as it is populated.
I am using setTimeout to loop through an array which works perfectly with the initial data load, but occasionally, when the array is updated with new information, the setTimeout glitches and either begins looping through from the beginning rather than continuing the loop, or creates a visual issue where the loop doubles.
Everything lives inside of a useEffect to ensure that data changes are only mapped when the listener finds new data. I am wondering if I need to find a way to get the setTimeout outside of this effect? Is there something I'm missing to avoid this issue?
const TeamDetails = (props) => {
const [teamState, setTeamState] = useState(props.pushData)
const [slide, setSlide] = useState(0)
useEffect(() => {
setTeamState(props.pushData)
}, [props.pushData])
useEffect(()=> {
const teams = teamState.filter(x => x.regData.onTeam !== "null" && x.regData.onTeam !== undefined)
const listTeams = [...new Set(teams.map((x) => x.regData.onTeam).sort())];
const allTeamData = () => {
let array = []
listTeams.forEach((doc) => {
//ALL CALCULATIONS HAPPEN HERE
}
array.push(state)
})
return array
}
function SetData() {
var data = allTeamData()[slide];
//THIS FUNCTION BREAKS DOWN THE ARRAY INTO INDIVIDUAL HTML ELEMENTS
}
SetData()
setTimeout(() => {
if (slide === (allTeamData().length - 1)) {
setSlide(0);
}
if (slide !== (allTeamData().length - 1)) {
setSlide(slide + 1);
}
SetData();
console.log(slide)
}, 8000)
}, [teamState, slide]);

Tracking changes in state?

The component code has several parameters, each of which has an initial value received from the server. How can I track that one of them (or several at once) has changed its state from the original one in order to suggest that the user save the changes or reset them?
Something similar can be seen in Discord when changing the profile / server.
The solution I found using useEffect () looks redundant, because there may be many times more options.
const [hiddenData, setHiddenData] = useState(server.hidden_data);
const [hiddenProfile, setHiddenProfile] = useState(server.hidden_profile);
const [isChanged, setIsChanged] = useState(false);
useEffect(() => {
if (hiddenData !== server.hidden_data
|| hiddenProfile !== server.hidden_profile) {
setIsChanged(true);
} else {
setIsChanged(false);
}
}, [hiddenData, server.hidden_data, hiddenProfile, server.hidden_profile]);
return (
<>
{isChanged && <div>You have unsaved changes!</div>}
</>
);
Maybe something like that?
const [draftState, setDraftState] = useState(server)
const [state, setState] = useState(server)
// a more complex object with the list of changed props is fine too
const isChanged = lodash.isEqual(state, draftState)
function changeProp (prop, value) {
setState({
...draftState,
[prop]: value
})
}
function saveState () {
setState(draftState)
// Persist state if needed
}

ReactJS how to run an API call when a state has a particular value

I'm teaching myself React and one of my exercises is using axios to fetch a list of countries from an API
const fetchCountries = () => {
axios.get("https://restcountries.eu/rest/v2/all").then(response => {
setCountries(response.data);
});
};
React.useEffect(fetchCountries, []);
Then as a user types into an input the list of countries filters down.
const handleInputChange = event => {
const filter = event.target.value; // current input value
let matchingCountries = query !== ''
? countries.filter(country => country.name.toLowerCase().indexOf(query.toLowerCase()) !== -1)
: countries;
setQuery(filter);
setMatches(matchingCountries)
console.log('matches', matches)
console.log('query', query)
};
My goal is that when a single country is matched, a new API request is triggered (to fetch the weather, but the what isn't my problem, the timing is). When a single country is matched, I will then render some data about the country, then fetch and render the weather details for the single country's capital city.
One of the problems I'm having is that when I set the state, the value always seems to be one step behind. For example, in this Codepen when you enter FRA you should get "France". However, I have to enter "FRAN" to get the match. This doesn't happen when I don't use a state variable for the matches (just let matches). This becomes a problem because I need to run the next API call when the number of matches = 1, but the length of the matches state is always wrong.
So I would like to know 1. how to get the correct state of the matched countries. And 2. when I should run the second API call without getting into an infinite loop.
useEffect solution using separation of concern
1 function should do 1 thing
handleInputChange updates state
useEffect updates state
But they are not coupled.
Later you might have a new function called handleDropdownChange which updates state
It that case you don't need to modify useEffect
At the end of the day, we (developers) don't like to rewrite things
const [countries, setCountries] = React.useState([]);
const [query, setQuery] = React.useState("");
const [matches, setMatches] = React.useState([]);
React.useEffect(() => {
let matchingCountries = query !== ''
? countries.filter(country => country.name.toLowerCase().indexOf(query.toLowerCase()) !== -1)
: countries;
setMatches(matchingCountries)
}, [query]); // called whenever state.query updated
const handleInputChange = event => {
setQuery(event.target.value); // update state
};
const fetchCountries = () => {
axios.get("https://restcountries.eu/rest/v2/all").then(response => {
setCountries(response.data);
});
};
React.useEffect(fetchCountries, []);
And there is also solution (not recommended) by directly using event.target.value provided by #Joseph D.
The only problem is you are using an old query value in handleInputChange().
Remember setting the state is asynchronous (i.e. doesn't take effect immediately)
Here's an updated version:
const handleInputChange = event => {
const filter = event.target.value; // current input value
let matchingCountries = filter ? <code here>
// ...
setQuery(filter);
};
UPDATE:
To call the weather api if there's a single country match is to have matches as dependency in useEffect().
useEffect(
() => {
async function queryWeatherApi() {
// const data = await fetch(...)
// setData(data)
}
if (matches.length === 1) {
queryWeatherApi();
}
},
[matches]
)
1) The reason for your problem is in this line:
let matchingCountries = filter !== ''
? countries.filter(country => country.name.toLowerCase().indexOf(query.toLowerCase()) !== -1)
: countries;
you use query instead of filter variable, your handler function should look like this:
const handleInputChange = event => {
const filter = event.target.value; // current input value
let matchingCountries = filter !== ''
? countries.filter(country => country.name.toLowerCase().indexOf(filter.toLowerCase()) !== -1)
: countries;
setQuery(filter);
setMatches(matchingCountries)
};
2) Where to run your next API call:
For studying purpose I do not want to recommend you using some application state management lib like redux.Just calling it right after setFilter and setQuery. It will run as expected. Because calling an API is asynchronous too so it will be executed after setQuery and setFilter what does not happen with console.log, a synchronous function.

Categories