Next.js Discord

Discord Forum

Timeout Issue in React Click Handler with useEffect

Answered
Rhinelander posted this in #help-forum
Open in Discord
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 };
};
View full answer

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 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.