Let’s build a stop watch clock that we can start/stop and also reset. This post is inspired by the example Kent C. Dodds gives in his free Beginner’s Guide to JS video series which I recommend watching.

To start, create a new React app called stopwatch using create-react-app.

$ npx create-react-app stopwatch
$ cd stopwatch
$ npm start

Open your web browser to http://localhost:3000/ to see the standard React welcome page. We only need to update the src/App.js file in this tutorial.

Stopwatch component

Our first task to is create a new Stopwatch component and include it as a child to the existing App component. Then we can add our timer along with “Start” and “Reset” buttons.

// src/App.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1>Stopwatch</h1>
        <Stopwatch />
      </div>
    );
  }
}

function Stopwatch() {
  return (
    <div>
      <p>0ms</p>
      <button>Start</button>
      <button>Reset</button>
    </div>
  );
}

export default App;

Static

props

Eventually we want the status (on/off) and running time of our stopwatch to be dynamic. A good first step is to add them as props we pass down from the parent App component. Then we can change them to state later on. We’ll set status to true and runningTime to 0 as the initial prop values. Also the status button features a ternary expression since it’s default state is false for off “Stop.”

// src/App.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1>Stopwatch</h1>
        <Stopwatch status={false} runningTime={0} />
      </div>
    );
  }
}

function Stopwatch({ status, runningTime }) {
  return (
    <div>
      <p>{runningTime}ms</p>
      <button>{status ? 'Stop' : 'Start'}</button>
      <button>Reset</button>
    </div>
  );
}

export default App;

State

Now we can transform Stopwatch into a class component with props.

// src/App.js
class Stopwatch extends Component {
  render() {
    const { status, runningTime } = this.props;
    return (
      <div>
        <p>{runningTime}ms</p>
        <button>{status ? 'Stop' : 'Start'}</button>
        <button>Reset</button>
      </div>
    );
  }
}

And then swap props for state. Remove the props in App too at this point as we no longer need them.

// src/App.js
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <h1>Stopwatch</h1>
        <Stopwatch />
      </div>
    );
  }
}

class Stopwatch extends Component {
  state = {
    status: false,
    runningTime: 0
  };
  render() {
    const { status, runningTime } = this.state;
    return (
      <div>
        <p>{runningTime}ms</p>
        <button>{status ? 'Stop' : 'Start'}</button>
        <button>Reset</button>
      </div>
    );
  }
}

export default App;

Dynamic buttons

To make our buttons dynamic they need to update state. So we’ll use onClick and add the event handlers handleClick and handleReset to each button. To make sure everything works we’ll also use setState to update state for each handler.

// src/App.js
class Stopwatch extends Component {
  state = {
    status: true,
    runningTime: 0
  };
  handleClick = () => {
    this.setState({ runningTime: 5, running: true });
  };
  handleReset = () => {
    this.setState({ runningTime: 0, running: false });
  };
  render() {
    const { status, runningTime } = this.state;
    return (
      <div>
        <p>{runningTime}ms</p>
        <button onClick={this.handleClick}>{status ? 'Start' : 'Stop'}</button>
        <button onClick={this.handleReset}>Reset</button>
      </div>
    );
  }
}

setInterval() and clearInterval()

Currently our state is being set each and every time a button is clicked. But we want a way to start/stop the time as well as reset it. We can use the DOM methods setInterval() and clearInterval() for this.

We’ll also create an updater function timer that accepts the current state and returns a new state with the updated time. This is necessary because state is immutable.

// src/App.js
class Stopwatch extends Component {
  state = {
    status: false,
    runningTime: 0
  };
  handleClick = () => {
    this.setState(state => {
      if (state.status) {
        clearInterval(this.timer);
      } else {
        const startTime = Date.now() - this.state.runningTime;
        this.timer = setInterval(() => {
          this.setState({ runningTime: Date.now() - startTime });
        });
      }
      return { status: !state.status };
    });
  };
  handleReset = () => {
    this.setState({ runningTime: 0, status: false });
  };
  render() {
    const { status, runningTime } = this.state;
    return (
      <div>
        <p>{runningTime}ms</p>
        <button onClick={this.handleClick}>{status ? 'Stop' : 'Start'}</button>
        <button onClick={this.handleReset}>Reset</button>
      </div>
    );
  }
}

The final step is get our “Reset” button to work. All we need is to clear out the state, which is what clearInterval() already does. So we can add this to handleReset and have it clear out the timer.

// src/App.js
handleReset = () => {
    clearInterval(this.timer); // new
    this.setState({ runningTime: 0, status: false });
  };

Dynamic

Memory Leaks

There is one error in our current implementation which is that the timer will run until it is explicitly stopped. The lifecycle method componentWillUnmount will clear all processes when the component is unmounted/removed from the DOM.

// src/App.js
class Stopwatch extends Component {
  ...
  componentWillUnmount() {
      clearInterval(this.timer);
    }
  render() { ... }
}

Conclusion

Welp that’s it. We started by creating a Stopwatch component and rendered it statically. Then we created props for status and runningTime so they could be updated dynamically. We transformed props into state and then added the updater function timer within handleClick to update the runningTime state. We added a clearInterval() to reset the time and also used componentWillUnmount to prevent any memory leaks.