Bugfix: Spelunking in Someone Else's Code

I love CodeSandbox (opens in a new tab). It has pretty much replaced CodePen for me unless I am fiddling around with CSS or freeCodeCamp front-end projects.

I like going through the sandboxes and picking out different ones to look at, take apart, and figure out how they work.

While going through React Tutorial for Beginners (opens in a new tab) by Kent C. Dodds (opens in a new tab) on Egghead.io (opens in a new tab) I decided I would look for sandboxes that correlate with the course as I was using Codesandbox to build out the stopwatch we were building in that course.

I found a sandbox (opens in a new tab) which I forked and found it to be buggy.

Why didn't the stopwatch work? Glancing at the code for a few seconds, I saw some obvious problems right away.

Here is an example of the stopwatch being broken:

Bugfix 1

The first thing I noticed was on line 7:

class StopWatch extends React.Component {
  state = { lapse: 0, running: false };
  handleRunClick = () => {
    const startTime = Date.now() - this.state.lapse;
    setInterval(() => {
      this.setState({
        lapse: Date.now - startTime
      });
    });
    this.setState({
      running: true
    });
  };

Date.now() needs parentheses. Date is an an object constructor with .now() being a method. When we click on the start button, React doesn't know what to do here; we aren't setting the state of lapse to be a number, which we expect. By adding the parentheses, we get the start button to work. No more NaNms.

But now we have another problem: the timer won't stop.

I also removed the console.log(Math.random()); because I felt it was unneeded.

Bugfix 2: Getting the Stopwatch to Stop and Clear

Each time the button is clicked, we set the state to either running or lapse. The timer runs when we click start but clicking stop or clear doesn't seem to work. How can we fix this?

We can create a timer update function that accepts the current state. We can accomplish this by using native DOM APIs such as setInterval() and clearInterval(). We can run conditional logic to see if the timer is running:

//updater function
this.setState(state => {
  if (state.running) {
    clearInterval(this.timer);
  } else {
    const startTime = Date.now() - this.state.lapse;
    this.timer = setInterval(() => {
      this.setState({
        lapse: Date.now() - startTime
      });
    });
  }

and use Date.now() to get the timestamp in ms, assign it a startTime variable to compare the current time to the amount of time that has passed. When we click the start button, it sets the startTime to the current timestamp. We also need to return a new state as state is not mutable..

class StopWatch extends React.Component {
  state = { lapse: 0, running: false };
  handleRunClick = () => {
    //updater function
    this.setState(state => {
      if (state.running) {
        clearInterval(this.timer);
      } else {
        const startTime = Date.now() - this.state.lapse;
        this.timer = setInterval(() => {
          this.setState({
            lapse: Date.now() - startTime
          });
        });
      }
      // returning a new state
      // to not mutate our original state
      return { running: !state.running };
    });
  };

Okay so this partially works. But as you can see below, if I click clear while the stopwatch timer is running, it doesn't clear the timer, and it also doesn't allow me to stop the timer, either.

How do we fix this particular bug?

If we look back at the previous code, we can see we are using clearInterval() to reset the stopwatch timer. In our current iteration, our handleOnClear method is just setting the state without clearing the previous state.

handleOnClear = () => {
  this.setState({
    lapse: 0,
    running: false
  });
};

We can fix this by adding clearInterval() and passing in the timer function to the handleOnClear method to clear the state.

handleOnClear = () => {
  clearInterval(this.timer);
  this.setState({
    lapse: 0,
    running: false
  });
};

This will give us the results we want.

Potential Problem?

There is a memory leak in this particular iteration. The timer will run until it is explicitly stopped in the DOM. We can use a React lifecycle method (opens in a new tab) to stop all processes in the DOM when this component is mounted or unmounted.

For this we can use componentWillUnmount to tell React to unmount the component once it is done rendering.

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

Thoughts and Conclusions

I find it much more enjoyable fixing other people's bugs than my own. This was a fun exercise and I plan on doing it more regularly and blogging about it.

This stopwatch is a stupid simple component but if you are just scratching the surface of React like me, I am sure digging into something like this stopwatch and figuring out how it works is an excellent exercise and use of one's time.

© tiff.RSS