How to useEffect pause setTimeout on handleMouseEnter event. Continue setTimeOout on handleMouseLeaveEvent? - javascript

I am trying to figure out how i can use the handleMouseEnter/Leave event to pause/continue the setTimeout. The rest of the code appears to be working fine for me.
function Education({ slides }) {
const [current, setCurrent] = useState(0);
const length = slides.length;
const timeout = useRef(null);
const [isHovering, setIsHovering] = useState(false);
useEffect(() => {
const nextSlide = () => {
setCurrent((current) => (current === length - 1 ? 0 : current + 1));
};
timeout.current = setTimeout(nextSlide, 3000);
return function () {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [current, length]);
function handleMouseEnter(e) {
setIsHovering(true);
console.log("is hovering");
}
function handleMouseLeave(e) {
setIsHovering(false);
console.log("not hovering");
}
}

Hey there you can do this by this simple implementation.
const {useEffect, useState, useRef} = React;
const Education = () => {
const slides = [1,2,3,4,5,6];
const [current, setCurrent] = useState(0);
const length = slides.length;
const timeout = useRef(null);
const [isHovering, setIsHovering] = useState(false);
useEffect(() => {
const nextSlide = () => {
setCurrent((current) => (current === length - 1 ? 0 : current + 1));
};
if ( !isHovering)
timeout.current = setTimeout(nextSlide, 2000);
return function () {
if (timeout.current) {
clearTimeout(timeout.current);
}
};
}, [current, length, isHovering]);
function handleMouseEnter(e) {
// stop the timeout function to be set
setIsHovering(true);
// clear any existing timeout functions
if ( timeout.current ){
clearTimeout(timeout.current);
}
}
function handleMouseLeave(e) {
// to trigger the useeffect function
setIsHovering(false);
}
return(
<div>
{
slides.map( (s, i) => {
if ( i === current){
return <div key={i} style={{padding:"2em", backgroundColor:"gray", fontSize:"2em"}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>{s}</div>
}
})
}
</div>
)
}
ReactDOM.render(<Education />, document.querySelector("#app"))
You can check out in JsFiddle

Related

How does setInterval work in reactjs (function components)?

I have got some states. I have got one button, which starts setInterval 1 second tick interval. Inside this setInterval I set these states.
let startGame;
const Cat = () => {
const [name, setName] = useState(null);
let [foodLevel, setFoodLevel] = useState(10);
let [healthLevel, setHealthLevel] = useState(10);
const [warning, setWarning] = useState('');
const [intervalId, setIntervalId] = useState(0);
const [value, setValue] = useState('');
const onChangeHandler = (event) => {
const { value } = event.target;
setValue(value);
};
const checkZeroFoodLevel = useCallback(() => {
return foodLevel <= 0;
}, [foodLevel]);
const tick = () => {
startGame = setInterval(() => {
if (!checkZeroFoodLevel()) {
setFoodLevel((prevFoodLevel) => prevFoodLevel - 1);
console.log(foodLevel); //foodLevel=10 every tick
} else {
setHealthLevel((prevHealthLevel) => prevHealthLevel - 1);
console.log(healthLevel);//healthLevel=10 every tick
}
}, 1000);
setIntervalId(startGame);
};
const startClickHandler = () => {
setName(value);
tick();
};
if (healthLevel === 0) {
clearInterval(intervalId);
}
return(...)
};
But when I click on this button, and output in a console my foodLevel state inside setInterval callback, this state does not update, but in a render is work fine. And when I output that from outside, it is updating as expect.
I found example with useRef():
let startGame;
const Cat = () => {
const [name, setName] = useState(null);
let [foodLevel, setFoodLevel] = useState(10);
let [healthLevel, setHealthLevel] = useState(10);
const currentFood = useRef(foodLevel);
currentFood.current = foodLevel;
const [warning, setWarning] = useState('');
const [intervalId, setIntervalId] = useState(0);
const [value, setValue] = useState('');
const onChangeHandler = (event) => {
const { value } = event.target;
setValue(value);
};
const checkZeroFoodLevel = useCallback(() => {
return currentFood.current <= 0;
}, [currentFood]);
const tick = () => {
startGame = setInterval(() => {
if (!checkZeroFoodLevel()) {
setFoodLevel((prevFoodLevel) => prevFoodLevel - 1);
console.log(currentFood);
} else {
setHealthLevel((prevHealthLevel) => prevHealthLevel - 1);
console.log(healthLevel);
}
}, 1000);
setIntervalId(startGame);
};
const startClickHandler = () => {
setName(value);
tick();
};
if (healthLevel === 0) {
clearInterval(intervalId);
}
return (...);
}
This is working fine. But I think that it is overhead. If I will have many states, I must create ref for each of them. I think, that it is not correct.
Can somebody explain to me why inside the function setInterval a state does not update? Can I do it without using useRef?

React setInterval question (using setState inside another setState problem)

I'm new to reactjs; I encountered this problem while studying about useState. I'm trying to decrease the value of the second state when the first state decreases to 0, and the iteration will run until both states are 0. But the second state always decreases by 2, which makes me confused.
This is my code:
import { useState } from "react";
import "./styles.css";
export default function App() {
const [firstCount,setFirstCount]=useState(10);
const [secondCount,setSecondCount] = useState(5);
function decreaseCount(){
const interval= setInterval(()=>{
setFirstCount((prevFirstCount)=>{
if(prevFirstCount>0){
return prevFirstCount-1;
}
else{
setSecondCount((prevSecondCount)=>{
if(prevSecondCount>0){
return prevSecondCount-1
}
else{
clearInterval(interval);
return prevFirstCount
}
})
return 10;
}
})
},1000)
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={(decreaseCount)}>Decrease Count</button>
</div>
);
}
codesandbox link: https://codesandbox.io/s/interval-setcountprob-plpzl?file=/src/App.js:0-835
I'd really appreciate if someone can help me out.
It's because the callback you pass to setFirstCount must be pure, but you violated that contract by trying to use it to mutate secondCount. You can correctly implement this dependency with useRef and useEffect:
export default function App() {
const [firstCount, setFirstCount] = useState(0);
const [secondCount, setSecondCount] = useState(6);
const firstCountRef = useRef(firstCount);
const secondCountRef = useRef(secondCount);
firstCountRef.current = firstCount;
secondCountRef.current = secondCount;
function decreaseCount() {
const interval = setInterval(() => {
if (secondCountRef.current === 0) {
clearInterval(interval);
return;
}
const { current } = firstCountRef;
setFirstCount(prev => (prev + 9) % 10);
setSecondCount(prev => current > 0 ? prev : (prev + 9) % 10);
}, 1000);
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={decreaseCount}>Decrease Count</button>
</div>
);
}
However, it might be easier to use a single state and compute the counts from that:
export default function App() {
const [count, setCount] = useState(60);
const countRef = useRef(count);
const firstCount = count % 10;
const secondCount = Math.floor(count / 10);
countRef.current = count;
function decreaseCount() {
const interval = setInterval(() => {
if (countRef.current === 0) {
clearInterval(interval);
return;
}
setCount(prev => prev - 1);
}, 1000);
}
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={decreaseCount}>Decrease Count</button>
</div>
);
}
I solved the issue this way :
const [firstCount, setFirstCount] = useState(10);
const [secondCount, setSecondCount] = useState(5);
const handleDecrease = () => {
setInterval(() => {
setFirstCount((prev) => {
if (prev > 0) {
return prev - 1;
}
if (prev === 0) {
return prev + 10;
}
});
}, 1000);
};
React.useEffect(() => {
if (firstCount === 0) {
setSecondCount((prev) => {
if (prev === 0) {
setFirstCount((firstPrev) => firstPrev + 10);
return prev + 5;
} else {
return prev - 1;
}
});
}
}, [firstCount]);
return (
<div className="App">
<div>{firstCount}</div>
<div>{secondCount}</div>
<button onClick={handleDecrease}>Decrease Count</button>
</div>
);
You shouldn't declare functions like this:
function decreaseCount(){
...
Instead you should use useCallback:
const decreaseCount = useCallback(() => {
//your code here
}[firstCount, secondCount]) //dependency array
You should read more about hooks: https://reactjs.org/docs/hooks-intro.html

ReactJS : eventListener tranistionend not getting cleaned up in useEffect

I have an image carousel component which has a smooth transition between images using the eventListener transtionend.
This event listener even though I have a cleanup function in place it creates a memory leak.
When I leave the page that has the image carousel the error does not appear yet. However, if I return to the page with the carousel and the transition completes one cycle (the image changes) then I get the error in the console.
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 attached my code below:
/** #jsx jsx */
import { useState, useEffect, useRef } from "react";
import { css, jsx } from "#emotion/core";
import SliderContent from "./SliderContent";
import Slide from "./Slide";
import Arrow from "./Arrow";
import Dots from "./Dots";
export default function Slider({ autoPlay }) {
const getWidth = () => window.innerWidth * 0.8;
const slides = [
"https://images.unsplash.com/photo-1449034446853-66c86144b0ad?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2100&q=80",
"https://images.unsplash.com/photo-1470341223622-1019832be824?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2288&q=80",
"https://images.unsplash.com/photo-1448630360428-65456885c650?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2094&q=80",
"https://images.unsplash.com/photo-1534161308652-fdfcf10f62c4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2174&q=80",
];
const firstSlide = slides[0];
const secondSlide = slides[1];
const lastSlide = slides[slides.length - 1];
const [isTabFocused, setIsTabFocused] = useState(true);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const [state, setState] = useState({
translate: 0,
transition: 0.9,
activeSlide: 0,
_slides: [firstSlide, secondSlide, lastSlide],
});
const { activeSlide, translate, _slides, transition } = state;
const autoPlayRef = useRef();
const transitionRef = useRef();
const resizeRef = useRef();
const focusedTabRef = useRef();
const blurredTabRef = useRef();
useEffect(() => {
//eslint-disable-next-line react-hooks/exhaustive-deps
if (transition === 0) setState({ ...state, transition: 0.9 });
}, [transition]);
useEffect(() => {
transitionRef.current = smoothTransition;
resizeRef.current = handleResize;
focusedTabRef.current = handleFocus;
blurredTabRef.current = handleBlur;
autoPlayRef.current = handleAutoPlay;
});
useEffect(() => {
const play = () => autoPlayRef.current();
let interval = null;
if (autoPlay) {
interval = setInterval(play, autoPlay * 1000);
}
return () => {
if (autoPlay) {
clearInterval(interval);
}
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [isButtonDisabled, autoPlay]);
useEffect(() => {
const smooth = (e) => {
if (typeof e.target.className === "string" || e.target.className instanceof String) {
if (e.target.className.includes("SliderContent")) {
transitionRef.current();
}
}
};
const resize = () => resizeRef.current();
const onFocusAction = () => focusedTabRef.current();
const onBlurAction = () => blurredTabRef.current();
const transitionEnd = window.addEventListener("transitionend", smooth);
const onResize = window.addEventListener("resize", resize);
const onFocus = window.addEventListener("focus", onFocusAction);
const onBlur = window.addEventListener("blur", onBlurAction);
return () => {
window.removeEventListener("resize", onResize);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
window.removeEventListener("transitionend", transitionEnd);
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isButtonDisabled) {
const buttonTimeout = setTimeout(() => {
setIsButtonDisabled(false);
}, 1000);
return () => clearTimeout(buttonTimeout);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [isButtonDisabled]);
const handleFocus = () => setIsTabFocused(true);
const handleBlur = () => setIsTabFocused(false);
const handleAutoPlay = () => isTabFocused && nextSlide();
const handleResize = () => setState({ ...state, translate: getWidth(), transition: 0 });
const nextSlide = () => {
if (!isButtonDisabled) {
setState({
...state,
translate: translate + getWidth(),
activeSlide: activeSlide === slides.length - 1 ? 0 : activeSlide + 1,
});
}
setIsButtonDisabled(true);
};
const prevSlide = () => {
if (!isButtonDisabled) {
setState({
...state,
translate: 0,
activeSlide: activeSlide === 0 ? slides.length - 1 : activeSlide - 1,
});
}
setIsButtonDisabled(true);
};
const smoothTransition = () => {
let _slides = [];
// We're at the last slide.
if (activeSlide === slides.length - 1)
_slides = [slides[slides.length - 2], lastSlide, firstSlide];
// We're back at the first slide. Just reset to how it was on initial render
else if (activeSlide === 0) _slides = [lastSlide, firstSlide, secondSlide];
// Create an array of the previous last slide, and the next two slides that follow it.
else _slides = slides.slice(activeSlide - 1, activeSlide + 2);
setState({
...state,
_slides,
transition: 0,
translate: getWidth(),
});
};
return (
<div css={SliderCSS}>
<SliderContent
translate={translate}
transition={transition}
width={getWidth() * _slides.length}
>
{_slides.map((slide, i) => (
<Slide width={getWidth()} key={slide + i} content={slide} />
))}
</SliderContent>
<Arrow direction="left" handleClick={prevSlide} isDisabled={isButtonDisabled} />
<Arrow direction="right" handleClick={nextSlide} isDisabled={isButtonDisabled} />
<Dots slides={slides} activeIndex={activeSlide} />
</div>
);
}
const SliderCSS = css`
position: relative;
height: 600px;
width: 80%;
margin: 40px auto 0px auto;
overflow: hidden;
`;
The window listener is getting removed at the end of the useEffect but I don't know why it still creates the memory leak.
Hmm. It seems you're removing event listeners incorrectly. DOM addEventListener returns nothing (undefined).
Wrong:
const onResize = window.addEventListener("resize", resize);
window.removeEventListener("resize", onResize);
Should be:
window.addEventListener("resize", resize);
window.removeEventListener("resize", resize);

Stop game loop on conditional with React

I am unable to stop the loop after a conditional. I am able to stop the interval after a button click but unable to stop it after a conditional such as loop increments. This simple example tries to stop the interval loop after 5 loops.
Any solutions would be much appreciated!
import React, { useState } from 'react';
let gameLoop: any;
function App() {
const [loopCount, setLoopCount] = useState(0);
const [running, setRunning] = useState(true);
const gameLogic = () => {
console.log('Game logic!')
}
const loop = () => {
gameLogic();
setLoopCount(prev => {
const newCount = prev + 1;
console.log(newCount)
return newCount
});
// Stop the loop on a conditional
if(loopCount >= 5){
clearInterval(gameLoop)
}
}
const handleStartButtonClick = () => {
gameLoop = setInterval(loop, 1000)
setRunning(true);
}
const handleStopButtonClick = () => {
clearInterval(gameLoop);
setRunning(false);
}
const handleResetButtonClick = () => {
setLoopCount(0);
console.clear();
}
return (
<div className="App">
<div>
<button onClick={handleStartButtonClick}>Start</button>
<button onClick={handleStopButtonClick}>Stop</button>
<button onClick={handleResetButtonClick}>Reset</button>
</div>
</div>
);
}
export default App;
The solution is to put the conditional at the component level, not in the loop method.
import React, { useState } from 'react';
let gameLoop: any;
function App() {
const [loopCount, setLoopCount] = useState(0);
const [running, setRunning] = useState(true);
const gameLogic = () => {
console.log('Game logic!')
}
const loop = () => {
gameLogic();
setLoopCount(prev => {
const newCount = prev + 1;
console.log(newCount)
return newCount
});
}
//MOVE OUTSIDE GAME LOOP
// Stop the loop on a conditional
if(loopCount >= 5){
clearInterval(gameLoop)
}
const handleStartButtonClick = () => {
gameLoop = setInterval(loop, 1000)
setRunning(true);
}
const handleStopButtonClick = () => {
clearInterval(gameLoop);
setRunning(false);
}
const handleResetButtonClick = () => {
setLoopCount(0);
console.clear();
}
return (
<div className="App">
<div>
<button onClick={handleStartButtonClick}>Start</button>
<button onClick={handleStopButtonClick}>Stop</button>
<button onClick={handleResetButtonClick}>Reset</button>
</div>
</div>
);
}
export default App;

Unable to access counter after gets updated

I want to start an interval by clicking on a button.
Here the interval gets started but, I can't access the value of counter. because when counter gets equal to 5, the interval should be stoped.
Here is the example:
let interval = null;
const stateReducer = (state, value) => value;
function App(props) {
const [counter, setCounter] = useReducer(stateReducer, 0);
const increment = () => {
interval = setInterval(() => {
setCounter(counter + 1);
if (counter === 5) clearInterval(interval);
console.log(counter);
}, 1000);
};
return (
<div>
<p>{counter}</p>
<button className="App" onClick={increment}>
Increment
</button>
</div>
);
}
You can run this code on codesandbox
change const stateReducer = (state, value) => value; to const
stateReducer = (state, value) => state+value;
make a variable let current_counter = 0; outside function
Change your increment function like this
current_counter = counter;
const increment = () => {
interval = setInterval(() => {
setCounter(1);
if (current_counter === 5) clearInterval(interval);
console.log(current_counter);
}, 1000);
};
Done
import React, { useReducer, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
let interval = null;
let current_counter = 0;
const stateReducer = (state, value) => value;
function App(props) {
const [status, setStatus] = useState(false);
const [counter, setCounter] = useReducer(stateReducer, 0);
useEffect(()=>{
if(status){
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000)
return ()=>{
clearInterval(interval)
};
}
})
const increment = () => {
setStatus(!status)
};
return (
<div>
<p>{counter}</p>
<button className="App" onClick={increment}>
Increment
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Your code has a few problems.
Your reducer declaration
const initialState = { value : 0 }
const reducer = (state, action) =>{
if(action.type === 'INCREMENT') return { value : state.value + 1 }
return state
}
How you're setting your reducer
const [state, dispatch] = useReducer(reducer, initialState)
How you're dispatching your action
intervals are imperative code, you can't consistently declare an interval inside a React's handler without worrying about closure. You could use the click only to flag that the interval should start and handle all imperative code inside an useEffect. Here is a working example
const initialState = { value: 0 };
const reducer = (state, action) => {
if (action.type === "INCREMENT")
return {
value: state.value + 1
};
};
function App() {
const [state, dispatch] = React.useReducer(reducer, initialState);
const [clicked, setClicked] = React.useState(false);
useEffect(() => {
let interval = null;
if (clicked) {
interval = setInterval(() => {
dispatch({ type: "INCREMENT" });
}, 1000);
}
if (state.value > 4) clearInterval(interval);
return () => clearInterval(interval);
}, [clicked, state]);
return <button onClick={() => setClicked(true)}>{state.value}</button>;
}
If your curious about closures and how react handle imperative code take a look on this awesome article from Dan Abramov (the most detailed explanation about effects out there).

Categories

Resources