One of the two timers doesn't execute rightly React

Viewed 43

CountDown.js:

import React from 'react';
import ReactDOM from 'react-dom';

export default class CountDown extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: props.duration ? props.duration : 5, //?
        }
    }

    componentDidMount() {
      this.timer =  setInterval(() => {
            let {count} = this.state;
            this.setState({
                count: count - 1
            })
        }, 1000)
    }

    componentDidUpdate(prevState)
    {
        if(prevState.count !== this.state.count && this.state.count === 0)
        {
            clearInterval(this.timer);
            if (this.props.onTimesup) {
                this.props.onTimesup();
            }
        }
    }

    fmtMSS(s) { return (s - (s %= 60)) / 60 + (9 < s ? ':' : ':0') + s }
    render(){
        let { count } = this.state;
        console.log(this.fmtMSS(count));
        return 
    }
}

App.js:

const App = () => {
    const [canType, setCanType] = useState(0);
            let onTimesup = () => {
            setCanType(1);
            console.log("timer1 " + canType);
        }

        let onTimessup = () => {
            setCanType(0);
            console.log("timer2 " + canType);
        }
    }
     return (
    <div className="box d">
            <CountDown onTimesup={onTimesup} duration={2} />
          {canType == 1 &&
                <input type="text" onChange={onChange}></input>}
            </div>
            {canType == 1 &&
              <CountDown onTimesup={onTimessup} duration={5} /> }
    }

What I want is to set a timer (timer1) of two seconds, then after it countdowns to 0 make "canType = 1" then set another timer (timer2) for 5 seconds and after it countdowns to 0 make "canType = 0" and repeat the cycle. Unfortunately, the system works like this: Console

1 Answers

I'd probably do a custom hook for this use case, skipping the class component and setInterval, which isn't terribly accurate for timing on its own because the callback is only guaranteed to run no sooner than the callback millisecond value. Using date comparisons and a polling interval helps mitigate this.

My approach here still isn't 100% accurate and drift can occur from one timer to the next (store the previous expiration time to avoid this) but the overall resolution should be good enough.

const useTimer = ({delay, onTick, onExpiration}) => {
  React.useEffect(() => { // or useLayoutEffect
    if (!delay || !delay.ms) {
      return;
    }

    let ticks = 0;
    const start = new Date();
    const id = setInterval(() => {
      const now = new Date();
      const elapsed = now - start;

      if (elapsed / 1000 > ticks) {
        onTick(ticks++);
      }

      if (elapsed >= delay.ms) {
        onExpiration(ticks);
        clearInterval(id);
      }
    }, 1000 / 60);
    return () => clearInterval(id);
  }, [delay]);
};

const App = () => {
  const [value, setValue] = React.useState("");
  const [disabled, setDisabled] = React.useState(true);
  const [countDown, setCountDown] = React.useState(3);
  const [delays, setDelays] = React.useState([
    {ms: 2000}, {ms: 5000}
  ]);

  useTimer({
    delay: delays[0],
    onTick: () => setCountDown(c => c - 1),
    onExpiration: () => {
      if (delays.length > 1) {
        setCountDown(delays[1].ms / 1000 + 1);
      }

      setDisabled(p => !p);
      setDelays(d => d.slice(1));
    },
  });

  return (
    <div>
      <input
        onChange={({target: {value}}) => setValue(value)}
        value={value}
        disabled={disabled}
      ></input>
      <p>{countDown}</p>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector("#app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="app"></div>

If you want to trigger the tick callback more often, divide by 100 or 10 instead of 1000. You can call it on every millisecond, but this seems excessive for displaying a value to the user. They probably only need to see tenths of seconds at the most.

Related