setInterval does not work properly in ReactJS - reactjs

I got a problem with setInterval in ReactJS (particularly Functional Component). I want to enable autoplay slider (it means value in Slider Bar will be increased after a period of time). This function will be triggered when I click a button. But I don't know exactly how setInterval works with setValue. The console log just returns the initial value (does not change) (console.log function was called in a callback function of setInterval).
My code is below:
const [value, setValue] = useState(10);
const [playable, setPlayable] = useState(false);
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable]);
const increaseValue = () => {
console.log(value);
setValue(value + 1);
};
const autoPlay = () => {
setPlayable(true);
};
return (
<div>
<div style={{"width": "800px"}}>
<Slider
value={value}
step={1}
valueLabelDisplay="on"
getAriaValueText={valueText}
marks={marks}
onChange={handleChange}
min={0}
max={100}
/>
</div>
<button onClick={autoPlay}>Play</button>
</div>
)

So, I think you need to pass increaseValue function into useEffect
useEffect(() => {
if (playable === true) {
console.log("Trigger");
const intervalId = setInterval(increaseValue, 1000);
return () => clearInterval(intervalId);
}
}, [playable, increaseValue]);

Related

Create the setTimeout in the parent and closes it in the children

I have a specific problem that is keeping me awake this whole week.
I have a parent component which has a pop-up children component. When I open the page the pop-up shows off and after 5 seconds it disappears with a setTimeout.
This pop-up has an input element in it.
I want the pop-up to disappear after 5 seconds or if I click to digit something in the input. I tried to create a timerRef to the setTimeout and closes it in the children but it didn't work.
Can you help me, please? Thanks in advance.
ParentComponent.tsx
const ParentComponent = () => {
const [isVisible, setIsVisible] = useState(true)
timerRef = useRef<ReturnType<typeof setTimeout>>()
timerRef.current = setTimeout(() => {
setIsVisible(false)
}, 5000)
useEffect(() => {
return () => clearTimeout()
})
return (
<div>
<ChildrenComponent isVisible={isVisible} inputRef={timerRef} />
</div>
)
}
ChildrenComponent.tsx
const ChildrenComponent = ({ isVisible, inputRef}) => {
return (
<div className=`${isVisible ? 'display-block' : 'display-none'}`>
<form>
<input onClick={() => clearTimeout(inputRef.current as NodeJS.Timeout)} />
</form>
</div>
)
}
You're setting a new timer every time the the component re-renders, aka when the state changes which happens in the timeout itself.
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
Instead you can put the initialization in a useEffect.
useEffect(() => {
if (timerRef.current) return;
timerRef.current = setTimeout(() => {
setIsVisible(false);
}, 5000);
return () => clearTimeout(timerRef.current);
}, []);
You should also remove the "loose" useEffect that runs on every render, this one
useEffect(() => {
return () => clearTimeout();
});

React click specific element in setInterval loop

I'm trying to click my element in setInterval loop, so it would be clicked every 10 second, but there's always error click is not a function or cannot read click null
I've tired with useRef and also did nothing.
here is my code:
useEffect(() => {
setInterval(function () {
const handleChangeState = () => {
console.log("Now");
document.getElementById("dice").click();
};
handleChangeState();
}, 10 * 1000);
}, []);
return (
<>
<Dice id="dice" rollingTime="3000" triggers={["click", "P"]} />
</>
);
};
It is often considered anti-pattern in React to query the DOM. You should instead use a React ref to gain access to the underlying DOMNode.
There are a couple ways to use a React ref to invoke a dice roll of the child component. FYI, rollingTime should probably be number type instead of a string if using in any setTimeout calls.
Forward the React ref and attach to the button element and invoke the click handler.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
return (
<>
<h1>Dice</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button ref={ref} id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleChangeState = () => {
console.log("Clicking Dice");
diceRef.current?.click();
};
setInterval(() => {
handleChangeState();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Forward the React ref and invoke the button's callback function directly via the useImperativeHandle hook.
Example:
const Dice = forwardRef(({ id, rollingTime }, ref) => {
const timerRef = useRef();
const [value, setValue] = useState();
const [isRolling, setIsRolling] = useState();
useEffect(() => {
return () => clearTimeout(timerRef.current);
}, []);
const roll = () => {
if (!isRolling) {
setIsRolling(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setValue(Math.floor(Math.random() * 6) + 1);
setIsRolling(false);
}, rollingTime);
}
};
useImperativeHandle(ref, () => ({
roll
}));
return (
<>
<h1>Dice 2</h1>
<h2>Roll Value: {isRolling ? "Rolling..." : value}</h2>
<button id={id} type="button" onClick={roll}>
Roll the dice
</button>
</>
);
});
...
export default function App() {
const diceRef = useRef();
useEffect(() => {
const handleRollDice = () => {
console.log("Roll dice");
diceRef.current.roll();
};
setInterval(() => {
handleRollDice();
}, 10 * 1000);
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}
Using react-dice-roll
If you examine the react-dice-roll source code you'll see that the Dice component forwards a React ref and uses the useImperativeHandle hook to expose out a rollDice function.
Dice Source
const Dice = forwardRef((props: TProps, ref: React.MutableRefObject<TDiceRef>) => {
...
const handleDiceRoll = (value?: TValue) => {
let diceAudio: HTMLAudioElement;
if (sound) {
diceAudio = new Audio(sound);
diceAudio.play();
}
setRolling(true);
setTimeout(() => {
let rollValue = Math.floor((Math.random() * 6) + 1) as TValue;
if (value) rollValue = value;
if (cheatValue) rollValue = cheatValue;
setRolling(false);
setValue(rollValue);
if (diceAudio) diceAudio.pause();
if (!onRoll) return;
onRoll(rollValue);
}, rollingTime);
};
useImperativeHandle(ref, () => ({ rollDice: handleDiceRoll }));
...
return (
...
)
});
Your code then just needs to create a React ref and pass it to the Dice component, and instantiate the interval in a mounting useEffect hook.
Example:
function App() {
const diceRef = useRef();
useEffect(() => {
const rollDice = () => {
console.log("Rolling Dice");
diceRef.current.rollDice(); // <-- call rollDice function
};
// instantiate interval
setInterval(() => {
rollDice();
}, 10 * 1000);
// immediately invoke so we don't wait 10 seconds for first roll
rollDice();
}, []);
return (
<div className="App">
<Dice
ref={diceRef}
id="dice"
rollingTime={3000}
triggers={["click", "P"]}
/>
</div>
);
}

Click event added in useEffect after changing state by onClick is executed in same moment

React 18 changed useEffect timing at it broke my code, that looks like this:
const ContextualMenu = ({ isDisabled }) => {
const [isExpanded, setIsExpanded] = useState(false);
const toggleMenu = useCallback(
() => {
if (isDisabled) return;
setIsExpanded((prevState) => !prevState);
},
[isDisabled],
);
useEffect(() => {
if (isExpanded) {
window.document.addEventListener('click', toggleMenu, false);
}
return () => {
window.document.removeEventListener('click', toggleMenu, false);
};
}, [isExpanded]);
return (
<div>
<div class="button" onClick={toggleMenu}>
<Icon name="options" />
</div>
{isExpanded && <ListMenu />}
</div>
);
};
The problem is, toggleMenu function is executed twice on button click - first one is correct, it's onClick button action, which changes state, but this state change executes useEffect (which adds event listener on click) and this click is executed on the same click, that triggered state change.
So, what should be correct and most "in reactjs spirit" way to fix this?
Your problem is named Event bubbling
You can use stopPropagation to fix that
const toggleMenu = useCallback(
(event) => {
event.stopPropagation();
if (isDisabled) return;
setIsExpanded((prevState) => !prevState);
},
[isDisabled],
);

React Hooks: Can't access updated useState variable

Issue: inputVal is a state variable that I update using setState.
It works fine when the button calls my handleConfirmSubmit function.
However, if I add that eventListener that listens for the keypress then triggers the function onKeyUp which then triggers handleConfirmSubmit... all of a sudden handleConfirmSubmit cannot see the updated state value.
I added the useEffect to console log out inputVal as I type, and I can confirm that it's updating, but for some reason, depending on where I invoke handleConfirmSubmit it either can or cannot see the updated state value.
CODE:
(Some unnecessary stuff has been stripped out)
export default function SingleInputModal(props){
const [cancelButton, setCancelButton] = useState(null);
const [inputVal, setInputVal] = useState(props.previousValue || '');
//ComponentDidMount
useEffect( () => {
//Listen for keypress
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keyup', onKeyUp);
}
}, []);
useEffect( () => {
console.log('new inputval', inputVal);
},[inputVal])
const handleChange = (e) => {
console.log('handle Change ran', e.target.value);
setInputVal(e.target.value || '');
}
const handleConfirmSubmit = () => {
console.log('About to submit: ', inputVal);
props.handleConfirmation(false, inputVal)
}
const onKeyUp = (e) => {
if(props.submitOnEnter && e.key === 'Enter'){
//Submit
handleConfirmSubmit();
}
if(props.cancelOnESC && e.key === 'Escape'){
//Cancel
props.handleConfirmation(true, null);
}
}
return(
<div className="confirm"
>
<div className="modal singleInput"
>
<div className="content">
<input
className='myModal__textInput'
type='text'
value={inputVal}
onChange={handleChange}
/>
</div>
<div className="actions">
<a
onClick={handleConfirmSubmit}
>Submit</a>
</div>
</div>
</div>
)
}
You can update your listener when inputVal changes, otherwise the listener will store a copy of the value of inputVal from the first render.
useEffect( () => {
//Listen for keypress
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keyup', onKeyUp);
}
}, [inputVal]);
You could use React.useCallback to make sure you have fresh versions of the functions (since e.g. handleConfirmSubmit can capture the inputVal in its closure):
export default function SingleInputModal(props) {
const [cancelButton, setCancelButton] = useState(null);
const [inputVal, setInputVal] = useState(props.previousValue || "");
useEffect(() => {
window.addEventListener("keyup", onKeyUp);
return () => window.removeEventListener("keyup", onKeyUp);
}, []);
useEffect(() => console.log("new inputval", inputVal), [inputVal]);
const handleChange = e => {
console.log("handle Change ran", e.target.value);
setInputVal(e.target.value || "");
};
const handleConfirmSubmit = React.useCallback(
() => {
console.log("About to submit: ", inputVal);
props.handleConfirmation(false, inputVal);
},
[inputVal],
);
const onKeyUp = React.useCallback(
e => {
if (props.submitOnEnter && e.key === "Enter") {
// Submit
return handleConfirmSubmit();
}
if (props.cancelOnESC && e.key === "Escape") {
// Cancel
props.handleConfirmation(true, null);
}
},
[handleConfirmSubmit, props.submitOnEnter, props.cancelOnESC],
);
return (
<div className="confirm">
<div className="modal singleInput">
<div className="content">
<input className="myModal__textInput" type="text" value={inputVal} onChange={handleChange} />
</div>
<div className="actions">
<a onClick={handleConfirmSubmit}>Submit</a>
</div>
</div>
</div>
);
}

how do I clearInterval on-click, with React Hooks?

I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);

Resources