I have a useTypewriter hook that seems to skip the second iteration since the i never logs as 1. Why is the code not working? The output should be howdy
, not hwdy
.
Here is the full code:
const { useState, useEffect } = React;
const useTypewriter = (text, speed = 50) => {
const [displayText, setDisplayText] = useState('');
useEffect(() => {
let i = 0;
const typingInterval = setInterval(() => {
console.log('text.length: ', text.length);
if (i < text.length) {
// problem: i is not 1 on the second iteration
setDisplayText((prevText) => {
console.log('prevText: ', prevText);
console.log(i, 'text.charAt(i): ', text.charAt(i));
return prevText + text.charAt(i);
});
i++;
} else {
clearInterval(typingInterval);
}
}, speed);
return () => {
clearInterval(typingInterval);
};
}, [text, speed]);
return displayText;
};
function App(props) {
const displayText = useTypewriter('howdy', 200);
return (
<div className='App'>
<h1>{displayText}</h1>
</div>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<App />
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm honestly struggling to spot the root cause (I bet once someone points it out it will be more obvious). But it looks like this may be a misuse of mutating a variable instead of relying on state. Specifically the variable i
.
If you put it in state in the hook:
const [i, setI] = useState(0);
And increment that state:
setI(i + 1);
And add it as a dependency for the useEffect
:
}, [text, speed, i]);
Then the desired functionality appears to work as expected.
Taking a step back... By making the effect depend on i
we're essentially turning that setInterval
into a simpler setTimeout
. Which makes more sense to me intuitively because state is updating and components are re-rendering on every interval anyway.
So we might as well simplify into a setTimeout
that gets triggered by the useEffect
, as mixing the effect with the interval was getting problematic:
useEffect(() => {
const timeout = setTimeout(() => {
if (i < text.length) {
setDisplayText((prevText) => prevText + text.charAt(i));
setI(i + 1);
}
}, speed);
return () => {
clearTimeout(timeout);
};
}, [text, speed, i]);