React Stopwatch
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;
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 ? 'Stop' : 'Start'}</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 });
};
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.
Want to learn more React? I have a list of recommended React books and JavaScript books.