Canceling a timeout in useEffect() if user scrolls with react hooks - javascript

Basically the same question as How to cancel a javascript function if the user scrolls the page but using react hooks.
I wrote react code that scrolls down to the end of the page after 3 seconds.
const scrollToEnd = () => { /* implementation omitted */ }
useEffect(() => {
const id = setTimeout(() => scrollToEnd(), 3000)
return () => clearTimeout(id)
}, [])
I want modify this code so that if the user manually scrolls the page before this timeout, the timeout is cleared.
I was thinking of a solution like:
const [hasScrolled, setHasScrolled] = useState(false);
const scrollToEnd = () => { /* implementation omitted */ }
useEffect(() => {
const setHasScrolledCallback = () => setHasScrolled(true)
window.addEventListener("scroll", setHasScrolledCallback);
return () => window.removeEventListener("scroll", setHasScrolledCallback);
}, []);
useEffect(() => {
const scrollCallback = () => { if (hasScrolled) scrollToEnd() }
const id = setTimeout(scrollCallback, 3000)
return () => clearTimeout(id)
}, [])
This works, but I don't think this is the correct way to approach this problem, because the scroll event is fired multiple times, even after the timeout occurs. Also the scrollCallback isn't really canceled, it runs anyway even if it does nothing.

Related

If I refresh the page matchMedia doesn't work

I'm using matchMedia in React to collapse my SideBar when the page is resizing. But the problem is if I refresh the page, my sidebar is open not closed. So if I want to collapse my SideBar I need to resize the page again or use the close button.
const layout = document.getElementById('home-layout');
const query = window.matchMedia('(max-width: 765px)');
query.onchange = (evt) => {
if( query.matches ) {
changeMenuMinified(true);
layout.classList.add('extended-layout');
}
else {
changeMenuMinified(false);
layout.classList.remove('extended-layout');
}
};
query.onchange();
};
useEffect(() => {
window.addEventListener('resize', handleResize);
});
If I remove addEventListener it works, I can reload the page and my sidebar stays closed but if I try to open the sidebar with a button, the sidebar closes quickly
const handleResize = () => {
const layout = document.getElementById('home-layout');
const query = window.matchMedia('(max-width: 765px)');
query.onchange = (evt) => {
if( query.matches ) {
changeMenuMinified(true);
layout.classList.add('extended-layout');
}
else {
changeMenuMinified(false);
layout.classList.remove('extended-layout');
}
};
query.onchange();
};
useEffect(() => {
handleResize()
});
sideBar
Some stuff to consider here:
Initialize your state with the current matching value
Remove listener on effect cleanup function
Don't forget the useEffect dependency array to avoid your code being executed on each render.
You can find a working example here -> https://codesandbox.io/s/stack-72619755-lpwh6m?file=/src/index.js:0-613
const query = window.matchMedia('(max-width: 765px)')
const App = () => {
const [minified, changeMenuMinified] = useState(query.matches)
useEffect(() => {
const resizeHandler = () => {
if (query.matches) {
changeMenuMinified(true)
} else {
changeMenuMinified(false)
}
}
query.addEventListener("change", resizeHandler);
return () => query.removeEventListener("change", resizeHandler);
})
return <p>{minified ? 'minified' : 'expanded'}</p>
}
That's because you need to have both in order to work, on load and also on reside, for that you can just do so:
Notice I added that empty dependencies array.
useEffect(() => {
handleResize();
window.addEventListener('resize', handleResize);
},[]);

Timer/Counter for react component - value remains 0 after increasing it with setInterval()

export default function Timer() {
const [timer, setTimer] = useState(0)
const checkTimer = () => {
console.log(timer);
}
useEffect(() => {
const timer = setInterval(() => {
setTimer(prevCount => prevCount + 1);
}, 1000);
startProgram(); //This starts some other functions
return () => {
checkTimer();
clearInterval(timer);
}
}, [])
}
Above is a simplified version of my code and the main issue - I am trying to increase the timer state by setting an interval in useEffect() (only once). However, in checkTimer() the value is always 0, even though the console statement execute every second. I am new to reactjs and would appreciate some help as this is already taking me too many hours to fix.
checkTimer is showing you the initial value of timer state because of stale closure. That means at the time when useEffect was executed, (i.e. once at component mount due to [] as dependency), it registered a cleanup function which created a closure around checkTimer function (and everything, state or props values, it uses). And when this closure was created the value of timer state was 0. And it will always remain that.
There are few options to fix it.
One quick solution would be to use useRef:
const timer = useRef(0);
const checkTimer = () => {
console.log(timer.current);
};
useEffect(() => {
const id = setInterval(() => {
timer.current++;
}, 1000);
return () => {
checkTimer();
clearInterval(id);
};
}, []);
Check this related post to see more code examples to fix this.
Edit:
And, if you want to show the timer at UI as well, we need to use state as we know that "ref" data won't update at UI. So, the option 2 is to use "updater" form of setTimer to read the latest state data in checkTimer function:
const [timer, setTimer] = useState(0);
const checkTimer = () => {
let prevTimer = 0;
setTimer((prev) => {
prevTimer = prev; // HERE
return prev; // Returning same value won't change state data and won't causes a re-render
});
console.log(prevTimer);
};
useEffect(() => {
const id = setInterval(() => {
setTimer((prev) => prev + 1);
}, 1000);
return () => {
checkTimer();
clearInterval(id);
};
}, []);

React Hook useEffect has a missing dependency: 'refreshSells'. Either include it or remove the dependency array

i want to add setInterval to be able to get new data from database without needing to refresh the page so i used useEffect,setInterval,useState to solve it,put intial state {refresh : false, refreshSells: null}
and there is switch when it on refresh = true and refreshSells= setinterval() but i got annoying warning
React Hook useEffect has a missing dependency: 'refreshSells'. Either include it or remove the dependency array
and if i add refreshSells it will be unstoppable loop
const Sells = () => {
const [allSells,setAllSells] = useState([])
const [refresh,setRefresh] = useState(false)
const [refreshSells , setRefreshSells] = useState(null)
const [hidden,setHidden] = useState(true)
useEffect(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
.catch(() => {
alert('something went wrong,ask omar')
})
},[])
useEffect(() => {
if(refresh){
setRefreshSells(setInterval(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
}, 60000));
}
else{
clearInterval(refreshSells)
}
return () => clearInterval(refreshSells)
},[refresh])
setRefreshSells updates internal state and doesn't change refreshSells during current render. So return () => clearInterval(refreshSells) will try to clear the wrong interval.
You should use useRef hook for your interval:
const refreshSellsRef = useRef(null);
...
useEffect(() => {
if(refresh){
refreshSellsRef.current = setInterval(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
}, 60000);
return () => clearInterval(refreshSellsRef.current);
}
},[refresh])
Also note that return () => clearInterval(refreshSellsRef.current) will be called on unmount and when refresh changes. So you don't need else {clearInterval(...)}
If your business logic allows to separate the 2 effects (automatic refresh every 60s + manual refresh after clicking some button), that would simplify the code for each independent effect:
useEffect(() => {
const interval = setInterval(() => {
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
}, 60000)
return () => clearInterval(interval)
}, [])
useEffect(() => {
if (refresh) {
setRefresh(false)
Axios.get('/sells')
.then(({data}) => {
setAllSells(data.sells)
})
};
}, [refresh])
It looks like you forgot to setRefresh(false) after triggering the refresh, but I am not sure why you needed refreshSells in the first place...
In the second useEffect you're updating the state refreshSells where useEffect expecting useCallback ref as a dependency. If you add refreshSells as a dependency to the useEffect then you may endup into memory leak issue.
So I suggest you try the below code which will solve your problem. by this you can also eliminate refreshSells
useEffect(() => {
let interval;
if (refresh) {
interval = setInterval(() => {
Axios.get('/sells')
.then(({ data }) => {
setAllSells(data.sells)
});
}, 4000);
}
return () => clearInterval(interval);
}, [refresh]);

React setState hook not working with useEffect

I have the following code. The idea is to create a simple loading for a mock component
const [loading, setLoading] = useState(false)
function mockLoading () {
setLoading(true)
console.log('1', loading)
setTimeout(() => {
setLoading(false)
console.log('2', loading)
}, 3000)
}
useEffect(() => {
mockLoading()
}, []);
But for some reason, setLoading is not working.
Console.log 1 and 2 both are false
A couple of things. One issue is that when you set the state, the state does not immediately update. So your first console.log will see the old state. Not only that, but when your effect is called and mockLoading is called, it will only see the instance of state that existed at the time it was called. So any changes to the variable will never be seen by mockLoading. This is why effects have dependencies. That way when a dependency updates, you'll see it. But I don't think having a dependency here will help given how your code is structured. I'm not 100% clear on your final goal, but to accomplish what you want based on the code you submitted, you'll want to useRef instead of useState. useRef gives you an object who's value is always current. See:
const loadingRef = useRef(false)
function mockLoading () {
loadingRef.current = true;
console.log('1', loadingRef.current)
setTimeout(() => {
loadingRef.current = false;
console.log('2', loadingRef.current)
}, 3000)
}
useEffect(() => {
mockLoading()
}, []);
Using refs is generally frowned upon unless you absolutely need it. Try to refactor your code to have a dependency on loading in your useEffect call. Though keep in mind you may end up with an infinite loop if your mockLoading always updates loading when it's called. Try to only update loading if it's not already set to the value you want. If your end goal is to just update loading after 3 seconds, try this:
const [loading,setLoading] = useState(false);
useEffect(() => {
setTimeout(() => {
setLoading(true);
},3000);
},[]);
return <span>{loading ? 'Loading is true!' : 'Loading is false!'}</span>;
If you want to inspect the value of loading without rendering it, then you'll need another useEffect with a dependency on loading:
const [loading,setLoading] = useState(false);
useEffect(() => {
setTimeout(() => {
setLoading(true);
},3000);
},[]);
useEffect(() => {
console.log(loading);
},[loading]);
return <span>{loading ? 'Loading is true!' : 'Loading is false!'}</span>;
Hope this helps.
Declare your handler inside useEffect and log outside
const [loading, setLoading] = useState(false)
useEffect(() => {
function mockLoading () {
setLoading(true)
console.log('1', loading)
setTimeout(() => {
setLoading(false)
console.log('2', loading)
}, 3000)
}
mockLoading()
}, []);
console.log(loading)
Also, you should clean your timeout on unmount
useEffect(() =>{
const timeout = setTimeout(() =>{}, 0)
return () => clearTimeout(timeout)
},[])
Setting state in React is async, so you won't see the changes to the state straight after you made them. To track state changes you need to use the effect hook:
const [loading, setLoading] = useState(false);
function mockLoading() {
setLoading(true);
console.log("1", loading);
setTimeout(() => {
setLoading(false);
console.log("2", loading);
}, 3000);
}
useEffect(() => {
mockLoading();
}, []);
// Display the current loading state
useEffect(() => {
console.log("loading", loading);
}, [loading]);

addEventListener does not work within a useEffect hook

The following is a component whose functionality, partly, is to change the window's title as the page is getting focused and blurred. It does not work.
const ATitleChangingComponent = () => {
const focusFunction = (event: FocusEvent) => {
document.title = "focused";
};
const blurFunction = (event: FocusEvent) => {
document.title = "blurred";
};
useEffect(() => {
window.addEventListener("focus", focusFunction);
return window.removeEventListener("focus", focusFunction);
}, []);
useEffect(() => {
window.addEventListener("blur", blurFunction);
return window.removeEventListener("blur", blurFunction);
}, []);
return <p>some unimportant jsx</p>
};
However,
const focusFunction = (event: FocusEvent) => {
document.title = "focused";
};
window.addEventListener("focus", focusFunction);
works just fine.
A side question: are const focusFunction and const blurFunction getting constructed within the function each render? I assume if so, they should be lifted out of the component to avoid unnecessary overhead?
Need to return a function, otherwise listener is removed immediately.
The function gets called when the component unmounts
useEffect(() => {
window.addEventListener("blur", blurFunction);
return () => window.removeEventListener("blur", blurFunction);
}, []);

Categories