JavaScript React Components Stateful Components and Lifecycle Methods Update the Stopwatch State with componentDidMount()

Having trouble understanding logic.

import React, { Component } from 'react';

class Stopwatch extends Component {
    state = {
        isRunning: false,
        elapsedTime: 0,
        previousTime: 0
    };

    componentDidMount(){
        this.intervalID = setInterval(()=> this.tick(), 100)
    }

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

    tick = () =>{
        if (this.state.isRunning){
            const now = Date.now();
            this.setState((prevState)=>({
                previousTime: now,
                elapsedTime: prevState.elapsedTime + (now - 
                this.state.previousTime)
            }));
        }
    }

    handleStopwatch = () => {
        this.setState(( prevState ) =>({ 
            isRunning: !prevState.isRunning
        }));
        if (!this.isRunning){
            this.setState({
                previousTime: Date.now()
            });
        }
    }
    handleReset = () => {
        this.setState({
            elapsedTime: 0

        });

    }

    render(){
        const seconds =  Math.floor( this.state.elapsedTime / 1000 );

        return (
            <div className= "stopwatch" >
                <h2>Stopwatch</h2>
                <span className="stopwatch-time">
                    { seconds }
                </span>
                <button onClick={ this.handleStopwatch }>
                    { this.state.isRunning ? 'Stop' : 'Start' }
                </button>
                <button onClick={ this.handleReset } >Reset</button>
            </div>
        );
    }

}







//can someone explain the working of this function
// the if statement part. we are passing date.now() 
 handleStopwatch = () => {
        this.setState(( prevState ) =>({ 
            isRunning: !prevState.isRunning
        }));
        if (!this.isRunning){
            this.setState({
                previousTime: Date.now()
            });
        }
    }

// here also we are updating the same state.
  tick = () =>{
        if (this.state.isRunning){
            const now = Date.now();
            this.setState((prevState)=>({
                previousTime: now,
                elapsedTime: prevState.elapsedTime + (now - 
                this.state.previousTime)
            }));
        }
    }

2 Answers

Gabbie Metheny
Gabbie Metheny
33,672 Points

First, I see an error in your handleStopwatch method: you should be checking for !this.state.isRunning, not !this.isRunning, since isRunning lives in the Stopwatch's state.

As to the logic, we're checking to see if the Stopwatch is not running because the handler runs at the point when "Start" or "Stop" is clicked. When you click "Start," isRunning will still be false, so the if block will execute. The previousTime will be updated to the exact moment that code ran, or the moment the stopwatch started.

Now that handleStopwatch has been called and isRunning has been set to true, the if block inside tick will execute, at the interval provided in componentDidMount (100ms). We save the exact time tick runs inside a variable, now, so that we can reference it in two different places in the code that follows. I think it helps to think about elapsedTime first, since it uses the existing value of previousTime before it's updated. So the elapsed time should be about 100ms more than the last value of elapsed time, but because of browser throttling with setInterval, we need to subtract the time that the stopwatch last updated from the current time before adding it to the elapsed time.

So ideally our count looks like: 100ms > 200ms > 300ms > 400ms etc.

But actually it probably looks something like: 100ms > 220ms > 320ms > 430ms etc.

So we're basically saying, if the stopwatch updated at 100ms (previousTime), and now it's 220ms (now), we should add the difference between those (120ms) to the old elapsedTime to get the new true elapsedTime. Additionally, we're updating previousTime to the current value of now, or Date.now() (like we did in handleStopwatch's if block) so that we can run the same calculation again in roughly 100ms.

Hope that made sense, let me know if there's anything I could clarify :)

Thank you i think i understand.

Masataka Miyahara
Masataka Miyahara
1,325 Points

I was confused about the same methods and seemed like I understood.

I think the most confusing point is that the setState method in React is asynchronous. From my perspective, that is why the following codes behave the same way*.

This code

handleStopwatch = () => {
  if (!this.state.isRunning) {
    this.setState({
      previousTime: Date.now()
    })
  }
  this.setState(prevState => ({
    isRunning: !prevState.isRunning
  }))
}

will behave the same way as the following code.

handleStopwatch = () => {
  this.setState(prevState => ({
    isRunning: !prevState.isRunning
  }))
  if (!this.state.isRunning) {
    this.setState({
      previousTime: Date.now()
    })
  }
}

In each case, if the user clicks the "start" button, !this.state.isRunning is evaluated as true because setState is asynchronous and this.setState(prevState => ({isRunning: !prevState.isRunning})) has not set isRunning to true yet.

* Strictly speaking, the two codes above differ from each other in terms of the the timing of adding the setState function to JavaScript Engine's stack.

I do not feel very confident of my understanding, so please let me know if there is anything wrong with my understanding:)