Reputation: 69
I was building UI for a dictionary that shows a word followed by its meaning. The word switches through a setInterval
. In the below code I'm setting up the setInterval
initially and then on some events. Here I'm just focused on the initial setInterval
.
Although I'm quite experienced in React I am still not able to grab my head around why the setInterval
works when inside useEffect
hook but not from the if condition.
To replicate just comment out the if condition and uncomment useEffect
block.
Can someone please explain?
import React, { useState, useEffect } from 'react';
import Dictionary from './Dictionary.json';
import './App.css';
const words = Object.keys(Dictionary);
let currentInterval;
function App() {
const [randomWord, setRandomWord] = useState(words && words[Math.round(Math.random() * words.length)]);
const [wordGenerationDuration, setWordGenerationDuration] = useState(10000);
function selectRandomWord() {
setRandomWord(words[Math.round(Math.random() * (words.length - 1))]);
}
if(!currentInterval) {
currentInterval = setInterval(selectRandomWord, wordGenerationDuration);
}
// useEffect(
// () => {
// currentInterval = setInterval(selectRandomWord, wordGenerationDuration);
// }, []
// )
return (
<div className='App'>
<div className='dictionary-wrapper'>
<p className='dictionary-word'>
{randomWord}
</p>
<p className='dictionary-meaning'>
{Dictionary[randomWord]}
</p>
<p className='next-button cursor-pointer'>
<span
onClick={
() => {
clearInterval(currentInterval);
selectRandomWord();
currentInterval = setInterval(selectRandomWord, wordGenerationDuration);
}
}
>
Next Word
</span>
</p>
<p>
<span>Auto Switch Duration:</span>
<input
className='dictionary-input'
type='number'
placeholder={wordGenerationDuration / 1000}
onChange={
(evt) => {
const wordGenerationDuration = Math.max(evt.target?.value * 1000, 10000);
clearInterval(currentInterval);
setWordGenerationDuration(wordGenerationDuration);
currentInterval = setInterval(selectRandomWord, wordGenerationDuration);
}
}
min='10'
/>
</p>
</div>
</div>
);
}
export default App;
Upvotes: 1
Views: 93
Reputation: 164901
Your original interval callback is a function defined within your component (selectRandomWord
).
When your component re-renders, the currentInterval
will be set so your interval will not be redefined. The existing interval will then refer to the previous render's version of selectRandomWord
which is no longer updating the state of the current render.
What you should really do is
useEffect()
, andfunction App() {
const interval = useRef()
const [randomWord, setRandomWord] = useState(words && words[Math.round(Math.random() * words.length)]);
const [wordGenerationDuration, setWordGenerationDuration] = useState(10000);
function selectRandomWord() {
setRandomWord(words[Math.round(Math.random() * (words.length - 1))]);
}
useEffect(() => {
if (!interval.current) {
interval.current = setInterval(selectRandomWord, wordGenerationDuration)
}
// cleanup
return () => {
clearInterval(interval.current)
}
}, [])
You can also clear the interval at any time using
clearInterval(interval.current)
See also https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
Upvotes: 1