0

My code is meant to start a timer for 20 minutes -> 20:00. I seem to be missing something in the code. My setState is not changing the value of time when I call it in Timer():

class Sleep extends Component {
  // create constructor to get access to props
  constructor(props) {
    super(props);
    this.state = {
      time: 1200,
      timer: '',
    };
  }

  componentDidMount() {
    this.interval = setInterval(this.Timer(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  Timer() {
    console.log('time is ' + this.state.time);

    this.setState({
      time: this.state.time - 1,
    });

    console.log('time is ' + this.state.time);
    var timer;

    var minutes = Math.floor(this.state.time / 60);
    var seconds = this.state.time % 60;

    if (minutes < 10) {
      minutes = '0' + minutes.toString();
    }

    if (seconds < 10) {
      seconds = '0' + seconds.toString();
    }

    timer = minutes.toString() + ':' + seconds.toString();

    this.setState({
      timer: timer,
    });
  }

  render() {
    return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#FFFFFF', paddingBottom: 100 }}>
        <View>
          <Text style={styles.timerText}>{this.state.timer}</Text>
        </View>
      </View>
    );
  }
}

The timer should just countdown from 20:00 to 00:00 every second. I threw in some console.log() statements to try and diagnose the issue.

Jee Mok
  • 5,433
  • 8
  • 44
  • 70
radhadman
  • 19
  • 2
  • maybe this will help you https://stackoverflow.com/questions/31963803/create-timer-with-react-native-using-es6 – Thinker Apr 09 '20 at 23:06

1 Answers1

0

Timer function definition doesn't have the this of the class bound to it, so when it is calls this.setState it isn't the same this as the component. You have 2 options.

Option 1: Bind this to Timer in the constructor

constructor(props) {
  super(props);
  this.state = {
    time: 1200,
    timer: '',
  };
  this.Timer = this.Timer.bind(this);
}

Option 2: Use ES6 arrow function to automatically bind this of caller

Timer = () => {...};

Note: function names, by convention, should be camelCased and not PascalCased

You can simplify your component logic a bit. Firstly, the displayed time can be derived from the numerical time state value, so no real need to store both. Secondly, looks like you are trying to update the time and then use that new time to compute the time to display. React state updates are asynchronous so the next state value won't be available until the next render cycle.

Just update the state in the setInterval callback and compute the derived display time in the render function.

Sleep.jsx

class Sleep extends Component {
  state = {
    time: 60 * 20 // 20 minutes
  };

  componentDidMount() {
    this.interval = setInterval(
      // functional state update to ensure state is correctly updated
      // from previous state
      () => this.setState(({ time }) => ({ time: time - 1 })),
      1000
    );
  }

  componentDidUpdate() {
    const { time } = this.state;
    if (time <= 0) {
      clearInterval(this.interval);
    }
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  render() {
    const { time } = this.state;
    const minutes = String(Math.floor(time / 60)).padStart(2, "0");
    const seconds = String(time % 60).padStart(2, "0");

    return (
      <div>
        {minutes}:{seconds}
      </div>
    );
  }
}

Edit countdown clock

Drew Reese
  • 103,803
  • 12
  • 69
  • 96
  • ok but where should I insert the time = time - 1? It does not work if I place it in setState it seems – radhadman Apr 10 '20 at 03:51
  • @radhadman Ah, I see. You are trying to update the `time` in state and then use that value to update the `timer` value in state. Updated answer with explanation. – Drew Reese Apr 10 '20 at 04:17
  • My other question is this: Why handle the ( if time <= 0) logic in componentDidUpdate() method rather than just doing so in the render()? Is it more efficient on performance or ? – radhadman Apr 10 '20 at 05:22
  • @radhadman It's a lifecycle function specifically to handle when state/props update. The [render](https://reactjs.org/docs/react-component.html#render) function should be a pure function with zero side-effects, like updating state or making API calls. – Drew Reese Apr 10 '20 at 06:11
  • I created a snooze button that becomes enabled after the timer hits 00:00 and got it to work without even changing anything in componentDidUpdate. Is this wrong to do? – radhadman Apr 12 '20 at 23:18
  • Another question for you Drew whenever you have time. Can you explain why what you suggested works: setInterval( () => this.setState( ({ time }) => ({ time: time - 1 }) ), 1000); I haven't really seen that notation in the React docs. Like why do you have to pass in the previous State. Why doesn't time: time - 1 just keep counting down the time one by one. Not sure why you have to pass in this ({ time }) business. Thanks – radhadman Apr 13 '20 at 06:54
  • @radhadman It's a [functional state update](https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous). All react state updates that *depend* on the previous state (like counters and timers) should definitely be using functional state updates. Here is a [demo](https://codesandbox.io/s/react-regular-and-functional-state-updates-2rtbk) I use to illustrate the difference between the two. The `({ time })` business is just object destructuring of `time` from the previous state object that is passed to the callback function. – Drew Reese Apr 13 '20 at 15:23
  • Thanks! Also, how should I go about rendering an audio file in Expo? – radhadman Apr 21 '20 at 01:24
  • @radhadman Playing audio files is out-of-scope for this question as each SO question should be limited to one specific topic/issue, but feel free to start another question. In the least you'll likely get more eyes on it being a new question. In the meantime, has this answer sufficiently addressed the timer issue you had? – Drew Reese Apr 21 '20 at 02:52