React state repeatedly reverts back to old value

Viewed 942

There is a component "DateForm" that changes the global state of "counterInfo" when the form is submitted.

//DateForm component submittal function. 
const submitDate = () =>{
    props.setCounterInfo(dateInfo); //passes date info to be counterInfo state in App.js            
    props.setShowInputForm(false);  //DateInfo component is no longer rendered
}

then, in app.js the counterInfo state is passed to the Timer component

const App = () => {
  const [showInputForm, setShowInputForm] = useState(false);
  const [counterInfo, setCounterInfo] = useState(undefined);
  return (
    <>
      <Timer
      counterInfo = {counterInfo}
      ></Timer>
      {showInputForm && 
      <DateForm
      setShowInputForm = {setShowInputForm}
      setCounterInfo = {setCounterInfo}
      ></DateForm>}
    </>
  );
}

There is a useEffect hook inside of the Timer function that, on a one second interval, used the value of counterInfo.

//Inside the Timer Component
const [currTime, setCurrTime] = useState(null);
useEffect (() => {
    setInterval(() => {
        let timeLeft = (new Date(`${Months(props.counterInfo.year)[props.counterInfo.month-1].name} ${props.counterInfo.day} ${props.counterInfo.year} ${props.counterInfo.hour}:${props.counterInfo.minute}:${props.counterInfo.second}`).getTime()) - new Date().getTime();
        setCurrTime(timeLeft);
    },1000);
    return(clearInterval());
}, [props, setCurrTime]);

What I intended to happen is for the value of timeLeft in Timer.js to update when the value of counterInfo is updated in DateForm, however, when the value is changed in DateForm, the result of both the new value of counterInfo and the old one both flash when the value of timeLeft is used in Timer.js. This issue isn't caused by any code in Timer.js becuase I tried moving the useEffect hook to app.js and passing the value down to Timer but the problem persisted. The only place that the setCounterInfo state is changed is in the DateForm component. Does anyone have any idea how to fix this?

1 Answers

First, you have bit mis-syntax at interval decleration


  useEffect (() => {
       let interval = setInterval(() => {...},1000);
       return () => clearInterval(interval);
   }, [props, setCurrTime]);

But unrelated, React by default re-applies effects after every render. This is intentional and helps avoid a whole class of bugs that are present in React components.

When it comes to intervals, its specifical matters cause if a render was applied every time setInterval is called, it never will get a chance to actually run

In other words, this code might have some side effects as useEffect in each run cares only of the existing values in that time and forget everything else, and interval isn't like so.

For that from my point of view the best practice is to create useInterval custom hook, that inside will store the callback for the meanwhile


function useInterval(callback) {
 const savedCallback = React.useRef();

 useEffect(() => {
   savedCallback.current = callback;
 });


 useEffect(() => {
   function run() {
     savedCallback.current();
  }
   let interval = setInterval(run ,1000);
   return () => clearInterval(interval);
  }, [])
 }



//Inside the Timer Component
 const [currTime, setCurrTime] = useState(null);

 useInterval(()=>
     setCurrTime((new Date(`${Months(props.counterInfo.year)[props.counterInfo.month-1].name} ${props.counterInfo.day} ${props.counterInfo.year} ${props.counterInfo.hour}:${props.counterInfo.minute}:${props.counterInfo.second}`).getTime()) - new Date().getTime()))
Related