Timeout Issue in React Click Handler with useEffect
Answered
Rhinelander posted this in #help-forum
RhinelanderOP
I'm encountering an issue in my React component involving timeouts and state updates. The component is designed for a quiz application, where users select answers, and the state is updated accordingly. Value a.answer get logged to the console just fine but selected is always null. It is because of setTimeout. With disable click i just want to make sure that user can not change answer after already selecting it. but this 2 timeouts are not in sync and my guess is that this is the only problem here. Would really appreciate feedback on what should I do. Corrected code snippet is also welcome.
useEffect(() => {
if (selected) {
let timeout = setTimeout(() => {
setDisableClick(false);
}, 2000);
return () => clearTimeout(timeout);
}
return;
}, [selected]);
const handleClick = (a: Answer) => {
console.log(a);
if (!disableClick && a.answer) {
setSelected(a.answer);
setDisableClick(true);
}
if (a.answer === selected && a.isCorrect) {
setScore((prev) => prev + 1);
}
const resetTimeout = setTimeout(() => {
setSelected(null);
next();
setDisableClick(false);
}, 2000);
return () => clearTimeout(resetTimeout);
};Answered by Palomino
export const Context = () => {
const [selected, setSelected] = useState(null);
const [disableClick, setDisableClick] = useState(false);
const [score, setScore] = useState(0);
const timerRef = useRef(null);
useEffect(() => {
if (!disableClick && timerRef.current) {
// disableClick && timerRef.current can only both be true if `handleClick` has been called, 2 seconds have passed, and we've called the block within the setTimeout
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const handleClick = (a) => {
// quiz logic, can freely use `useState` callbacks here
setSelected(a.answer); // etc
timerRef.current = setTimeout(() => {
setSelected(null);
setDisableClick(false);
next();
}, 2000);
};
return { selected, setSelected, disableClick, handleClick };
};5 Replies
Palomino
In general having a singular React component manage its own delays via timeouts and have intermediary rendering states is almost certainly going down the wrong path. This also makes unit testing the component virtually impossible, as the component intermediary state is forever transitory, and thus your tests to check if a component is briefly in the
Is there any possibility that you can bring any of this functionality out into a longer-living construct, something like a React Context? Or just a parent component with some hoisted state? At the moment, it looks like this component has race conditions (with two
If you can use an external construct manage this functionality, the only
disableClick state would be incredibly flaky / run into race conditions.Is there any possibility that you can bring any of this functionality out into a longer-living construct, something like a React Context? Or just a parent component with some hoisted state? At the moment, it looks like this component has race conditions (with two
setTimeouts) and very likely a memory leak (the return in the handleClick function, unless you're calling that from outside this code example and waiting at least 2 seconds again before cleaning it up w/ clearTimeout).If you can use an external construct manage this functionality, the only
setTimeout you would need is to wait for the next() invocation (and reset any values you need), and then you can save any persistent state within that construct (score, selected, disableClick) that can change the child component's state freely. Something like:Palomino
export const Context = () => {
const [selected, setSelected] = useState(null);
const [disableClick, setDisableClick] = useState(false);
const [score, setScore] = useState(0);
const timerRef = useRef(null);
useEffect(() => {
if (!disableClick && timerRef.current) {
// disableClick && timerRef.current can only both be true if `handleClick` has been called, 2 seconds have passed, and we've called the block within the setTimeout
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const handleClick = (a) => {
// quiz logic, can freely use `useState` callbacks here
setSelected(a.answer); // etc
timerRef.current = setTimeout(() => {
setSelected(null);
setDisableClick(false);
next();
}, 2000);
};
return { selected, setSelected, disableClick, handleClick };
};Answer
Palomino
If you do insist on maintaining only one component, something like this might work, but it's very messy and probably very buggy, and would extremely strongly recommend against it:
export const Component = () => {
const [selected, setSelected] = useState(null);
const [disableClick, setDisableClick] = useState(false);
const [score, setScore] = useState(0);
const initialRef = useRef(null);
const finalRef = useRef(null);
useEffect(() => {
return () => {
if (!disabledClick) {
clearTimeout(initialRef);
clearTimeout(finalRef);
}
};
}, []);
const handleClick = (a) => {
initialRef.current = setTimeout(() => {
if (!disableClick && a.answer) {
setSelected(a.answer);
setDisableClick(true);
}
if (a.answer === selected && a.isCorrect) {
setScore((prev) => prev + 1);
}
}, 1);
finalRef.current = setTimeout(() => {
setSelected(null);
setDisableClick(false);
next();
}, 2000);
};
return (
<button onClick={() => handleClick("a")} disabled={disableClick}>
Answer
</button>
);
};There are also individual custom Hooks examples in this article that you can draw from https://felixgerschau.com/react-hooks-settimeout/
RhinelanderOP
Thank you. Context is great solution didn't even tough of that.