JS-201-ReactFundamentals
  • Part 0: App Overview
  • Part 1: Intro to React
    • 1.0: Packages Setup
    • 1.1: Project Setup
    • 1.2: React Router Dom
  • Part 2: JavaScript Concepts
    • 2.0: ES6 Overview
    • 2.1: classes
    • 2.2: constructors
    • 2.3: extends
    • 2.4: super
    • 2.5: interpolation
    • 2.6: map
    • 2.7: filter
  • Part 3: Functional Components
    • 3.0: Functional Components Overview
    • 3.1: Calling Functional Components
    • 3.2: Single Element Rule
    • 3.3: Arrow Functions
    • 3.4: Challenge
    • 3.4: Solution
    • 3.5: Challenge 2
    • 3.5: Solution 2
  • Part 4: JSX Challenge
    • 4.1: JSX Overview
    • 4.1: Challenge Answer
    • 4.2: className
  • Part 5: Class Concepts
    • 5.1: State
    • 5.2: setState
    • 5.3: Class Components Challenge
    • 5.4: Class Components Challenge Answer
  • Part 6: Props Starter
    • 6.0: Props Overview
    • 6.1: Props Demo and Challenge 1
    • 6.2: Props Answer 2
    • 6.3: Props Passing Functions and Challenge 2
    • 6.4: Props Answer 2
    • 6.5: External Props and Mapping Components
    • 6.6: PropTypes
  • Part 7: Lifecycle Methods
    • 7.0: Lifecycle Overview
    • 7.1: Lifecycle Methods
    • 7.2: Mounting Methods
    • 7.3: Update Methods
    • 7.4: Unmount Methods
  • Part 8: Apps
    • 1.0 - Small Timer Apps
      • 1.1 - Simple Timer
      • 1.2 - Clock App
      • 1.3 - Stop Watch App
      • 1.4 - Challenge
    • 2.0 - Concept List
      • 2.1 - Concept Data
      • 2.2 - Concept App Component
      • 2.3 - Concept Component
      • 2.4 - Concept List Component
    • 3.0 - NYT
      • 3.1 - NytApp Component
      • 3.2 - Nyt Results
    • 4.0 - The Friend List App
      • 4.1 - Friend List App Component
      • 4.2 - Friend Component
    • 5.0 - Movie Search Application
      • 5.1 - Form Component
      • 5.2 - Form Results Component
      • 5.3 - Styled Components
    • 6.0 - YouTube Api
      • 6.1 - Video Component
      • 6.2 - Video Component
      • 6.3 - Video Component
    • 7.0 - Github Api Application
      • 7.1 - The Users
      • 7.2 - Github API Component
      • 7.3 - Github API Card
      • 7.4 - Github API Card Form
      • 7.5 - Github API Card List
      • 7.6 - Github API Search
      • 7.7 - Github API Challenge
    • 8.0 - Bitcoin Api Application
      • 8.1 - Bitcoin API Setup
      • 8.2 - Bitcoin API Fetch
      • 8.3 - Bitcoin API Line Chart
      • 8.4 - Bitcoin API Fetching Data
      • 8.5 - Bitcoin API Info Box
      • 8.6 - Bitcoin API Completed Code
    • 9.0 - Google Maps Api Challenge
    • 10.0 - Sound Cloud App Challenge
    • 11.0 - VR App Challenge
    • 12.0 - React Native Intro
  • Part 9: Project
  • Part 10: Notes
    • 10.1 - Resources
    • 10.2 - Troubleshooting
Powered by GitBook
On this page
  • App Setup
  • App Planning
  • Constructor
  • Display
  • Child Components
  • Time Methods
  • Toggle Method
  • startTimer()
  • Update Method
  • lap method
  • reset method
  • Run the app
  • ES6 Version
  1. Part 8: Apps
  2. 1.0 - Small Timer Apps

1.3 - Stop Watch App

This next time app we're going to make is going to be a stopwatch!

App Setup

To start we need to create a StopWatchApp.js file inside of our timer-apps folder, your timer-apps should look like the following.

    └── src
        └── assets
        └── components
                └── apps
                    └── timer-apps
                        └── TimePiecesApp.js
                        └── TimerApp.js 
                        └── ClockApp.js  
                        └── StopWatchApp.js

Let's go ahead and get our StopWatchApp.js file started with the following code:

import React, { Component } from 'react';

export default class Stopwatch extends Component {
  render() {
    return (
      <div>
        <h1 className="section-title">React Stopwatch!</h1>
      </div>
    );
  }
}

Next, to get this to display, we need to change our parent component, TimePiecesApp to no longer have StopWatchApp commented out, so go ahead and make sure that your TimePiecesApp file looks like this.

import React from 'react';
import TimerApp from './TimerApp';
import ClockApp from './ClockApp';
import StopWatchApp from './StopWatchApp';


const TimePiecesApp = () => {
    return (
        <div className="main">
            <div className="mainDiv">
                <TimerApp />
                <hr />
                <ClockApp />
                 <hr />
                <StopWatchApp /> 
            </div>
        </div>
    )
}

export default TimePiecesApp;

Now, you should see your Stopwatch App h1, and we're ready to start developing the stopwatch.

App Planning

To start thinking about our stop watch app, we're going to want to consider what pieces of data we'll need to display for our stopwatch to work, and what kind of data we want in our state. When starting a React application, taking time to think through and carefully design the state will save you time and headache later.

We need a few capabilities for a stop watch, one the ability to keep track of time starting at a specified moment, two, different lap times need to be able to be recorded, and three we need to know whether or not the stopwatch is currently running. Our state needs to include these things so that we can easily display them to the screen.

Constructor

Go ahead and start a constructor for this component, keeping in mind the state that we need. We're also going to bind our methods to this in the constructor. Type out the following code in your constructor.

    constructor(props) {
        super(props);

        ["lap", "update", "reset", "toggle"].forEach((method) => {
            this[method] = this[method].bind(this);
        });

        this.state = this.initialState = {
            isRunning: false,
            lapTimes: [],
            timeElapsed: 0,
        };
    }

Notice here that we also are binding the methods to this. The event handler has its context bound to the component instance. You can access props and state and call setState or forceUpdate from the bound handler. So in order to access setState from event handlers, they need to be bound to the this. So we can either bind the methods that need access to setState, or we can make them as arrow functions, since arrow functions do not bind their this, and therefore in them, you will still have access to setState.

In this instance, we'll go ahead and bind them in the constructor. At the end of the module, there will be code using the ES6 way of just using arrow functions.

Display

Next, we need a way to display our state, so we can add some things to our render() function. Since, we have multiple parts of our state object, and writing this.state.isRunning, this.state.lapTimes, multiple times, we can just declare constants to make it easier to access our state in our render. In the first line of the render, we do this, as seen below. Go ahead and write the following in your render function, then we'll go through it.

render() {
        const { isRunning, lapTimes, timeElapsed } = this.state;
        return (
            <div>
                <h1>React Stopwatch</h1>
                <TimeElapsed id="timer" timeElapsed={timeElapsed} />
                <button onClick={this.toggle}>
                    {isRunning ? 'Stop' : 'Start'}
                </button>
                <button
                    onClick={isRunning ? this.lap : this.reset}
                    disabled={!isRunning && !timeElapsed}
                >
                    {isRunning || !timeElapsed ? 'Lap' : 'Reset'}
                </button>
                {lapTimes.length > 0 && <LapTimes lapTimes={lapTimes} />}
            </div>
        );
    }

So, in our return, we first have our header, and then we have a TimeElapsed component that we have not yet defined. When we see a capitalized component like this, it means it is a react component, one that we have yet to create. We can kind of tell what it's doing though, it has an id of "timer", and it takes in the timeElapsed. We can probably guess that it's going to display the timeElapsed, since that's all it is taking as a prop.

Next, we have a button that has an on click event called "toggle", it then display "Stop" or "Start" based on whether or not isRunning is true. This appears to be how we can stop and start our timer.

The next button has an onClick event of either lap or reset, depending on if isRunning is true, and again the text "Lap" or "Reset". This button will be able to do a couple of different things depending on if isRunning is true.

Last of all, we have a LapTimes component that takes in lapTimes, and presumably, displays them. Again, this is another component we have yet to define, and one that we'll need to.

Note the frequent use of ternary operators, for conditional rendering of things.

Child Components

Next, let's go ahead and create the other react components that we're using to render certain props. Since we don't need to access state in either of state, and they're primarily used to render data, we can make these functional components using arrow functions.

Let's start with the LapTimes component. We want this laptime component to display the lap times, as we press lap and add to them. We're passing lapTimes to this component as a prop, so it needs to take that and format it in a way to display nicely. Let's start by starting off our LapTimes component and using map, map our array of lapTimes into a tr with tds containing the information we want to display. Then to display this, after we've created a variable with the results of our map, we can just simply display that in our return. Go ahead and type out the code for LapTimes outside of the code for your StopWatchApp component.

const LapTimes = (props) => {
        const rows = props.lapTimes.map((lapTime, index) =>
            <tr key={++index}>
                <td>{index}</td>
                <td><TimeElapsed timeElapsed={lapTime} /></td>
            </tr>
        );
        return (
            <table id="lap-times">
                <thead>
                    <tr>
                        <th>Lap</th>
                        <th>Time</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
}

So now, our react app should know what LapTimes is, and should be able to find it. It still doesn't know what TimeElapsed is, so let's go ahead and create that component.

For TimeElapsed, we are passing it timeElapsed as a prop, and want it to be able to display the time elapsed! First thing we need to do with our timeElapsed, is make sure it's in seconds so that we can convert it, and display it how we want it to be displayed. Then we'll need to figure out minutes from there to display. We can create a function for this called getUnits.

Next we'll need to zero pad our timer, with 0s as needed to make it look like a normal stopwatch. If we don't do this, we will only start with a single zero for seconds and minutes, which is not how time works. We can create a function called leftPad to do this. Lastly, we need to display our time elapsed, in our render function, we can do this implementing the other functions we created. Type the code below, underneath LapTimes.

const TimeElapsed = (props) => {
    const getUnits = () => {
        const seconds = props.timeElapsed / 1000;
        return {
            min: Math.floor(seconds / 60).toString(),
            sec: Math.floor(seconds % 60).toString(),
            msec: (seconds % 1).toFixed(3).substring(2)
        };
    }
        const leftPad = (width, n) => {
            if ((n + '').length > width) {
                return n;
            }
            const padding = new Array(width).join('0');
            return (padding + n).slice(-width);
        };
        const units = getUnits();
        return (
            <div id={props.id}>
                <span>{leftPad(2, units.min)}:</span>
                <span>{leftPad(2, units.sec)}.</span>
                <span>{units.msec}</span>
            </div>
        );
}

Make sure you read through and understand the above code before moving on. We've created these two functional components to format and display our data how we need it. It's nice to separate concerns into different components so that your code is more easily readable and reusable.

Time Methods

At this point, you should now no longer have any errors, but nothing should happen when you click the Start button, the stopwatch doesn't yet work. So all of our render portions are there, now we need to create the methods to make this thing actually work. If you look back to our constructor, you'll have hints as to what a couple of these methods are. Let's go through them one by one.

Toggle Method

First up we have our toggle method. We need this to basically toggle back and forth whether or not the stop watch is running or not. So when we call this it needs to change the isRunning part of our state.

So the first part of toggle method needs to set isRunning to the opposite of what it is, like so:

    this.setState({ isRunning: !this.state.isRunning }, () => {
        });

Then we need it to either start the timer if isRunning is now true, or to stop the timer (clearInterval), if isRunning is now false. Type the following code below into your StopWatchApp below your constructor.

toggle() {
        this.setState({ isRunning: !this.state.isRunning }, () => {
            this.state.isRunning ? this.startTimer() : clearInterval(this.timer)
        });
    }

startTimer()

Now that we have toggle, we actually need a startTimer() method for toggle to call to start the timer. This is going to look similar to the timers we have used before, but this time instead of updating every 1000ms, we want it to update more frequently since we're intersted in a smaller increment of time for a stopwatch. Type the following code below your toggle() method in your StopWatchApp.

    startTimer() {
        this.startTime = Date.now();
        this.timer = setInterval(this.update, 10);
    }

Update Method

We now need an update method, to use in our startTimer(). Timer will use this update method every 10ms, to update the displayed time. update() is going to use the initial startTime, and then set the new startTime, for future use. Every time it will set a new timeElapsed, based on the old time elapse, and the difference between the startTime and the current time. This component is essential to constantly update the timeElapsed.

    update() {
        const delta = Date.now() - this.startTime;
        this.setState({ timeElapsed: this.state.timeElapsed + delta });
        this.startTime = Date.now();
    }

lap method

Next we need our lap() method. This method will be called when the lap button is pressed, and will add on a new time to the lapTimes every time it is called. If you remember our initial state, lapTime start as an empty array, so we'll need to add a new lap time to the array every time. Add the following code below your update() method.

    lap() {
        const { lapTimes, timeElapsed } = this.state;
        this.setState({ lapTimes: lapTimes.concat(timeElapsed) });
    }

reset method

Lastly, we need a reset() method, to completely reset the stopwatch when the reset button is clicked. This method will clearInterval on our timer, like in our other timers, and set the state back to our initial empty state. Add the following code below your lap() method.

    reset() {
        clearInterval(this.timer);
        this.setState(this.initialState);
    }

Run the app

Now you should have a working stop watch!!!

Make sure you carefully look through the code, and understand how this app is working. This app is definitely more complex than the previous two, but uses all of the same concepts. If you don't understand how any part of this app works, ask a neighbor or an instructor. The following code is the original with the functions bound in the constructor.

// https://jsfiddle.net/vakhtang/j276r2zh/
import React, { Component } from 'react';

export default class Stopwatch extends Component {
    constructor(props) {
        super(props);

        ["lap", "update", "reset", "toggle"].forEach((method) => {
            this[method] = this[method].bind(this);
        });

        this.state = this.initialState = {
            isRunning: false,
            lapTimes: [],
            timeElapsed: 0,
        };
    }
    toggle() {
        this.setState({ isRunning: !this.state.isRunning }, () => {
            this.state.isRunning ? this.startTimer() : clearInterval(this.timer)
        });
    }
    lap() {
        const { lapTimes, timeElapsed } = this.state;
        this.setState({ lapTimes: lapTimes.concat(timeElapsed) });
    }
    reset() {
        clearInterval(this.timer);
        this.setState(this.initialState);
    }
    startTimer() {
        this.startTime = Date.now();
        this.timer = setInterval(this.update, 10);
    }
    update() {
        const delta = Date.now() - this.startTime;
        this.setState({ timeElapsed: this.state.timeElapsed + delta });
        this.startTime = Date.now();
    }
    render() {
        const { isRunning, lapTimes, timeElapsed } = this.state;
        return (
            <div>
                <h1>React Stopwatch</h1>
                <TimeElapsed id="timer" timeElapsed={timeElapsed} />
                <button onClick={this.toggle}>
                    {isRunning ? 'Stop' : 'Start'}
                </button>
                <button
                    onClick={isRunning ? this.lap : this.reset}
                    disabled={!isRunning && !timeElapsed}
                >
                    {isRunning || !timeElapsed ? 'Lap' : 'Reset'}
                </button>
                {lapTimes.length > 0 && <LapTimes lapTimes={lapTimes} />}
            </div>
        );
    }
}
const TimeElapsed = (props) => {
    const getUnits = () => {
        const seconds = props.timeElapsed / 1000;
        return {
            min: Math.floor(seconds / 60).toString(),
            sec: Math.floor(seconds % 60).toString(),
            msec: (seconds % 1).toFixed(3).substring(2)
        };
    }
        const leftPad = (width, n) => {
            if ((n + '').length > width) {
                return n;
            }
            const padding = new Array(width).join('0');
            return (padding + n).slice(-width);
        };
        const units = getUnits();
        return (
            <div id={props.id}>
                <span>{leftPad(2, units.min)}:</span>
                <span>{leftPad(2, units.sec)}.</span>
                <span>{units.msec}</span>
            </div>
        );
}

const LapTimes = (props) => {
        const rows = props.lapTimes.map((lapTime, index) =>
            <tr key={++index}>
                <td>{index}</td>
                <td><TimeElapsed timeElapsed={lapTime} /></td>
            </tr>
        );
        return (
            <table id="lap-times">
                <thead>
                    <tr>
                        <th>Lap</th>
                        <th>Time</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
}

ES6 Version

Now, as promised, this is the code refactored to use ES6 arrow functions, which do not bind this, and therefore do not need to be bound in the constructor.

import React, { Component } from 'react';

export default class Stopwatch extends Component {
    constructor(props) {
        super(props);
        this.state = this.initialState = {
            isRunning: false,
            lapTimes: [],
            timeElapsed: 0,
        };
    }
    toggle = () => {
        this.setState({ isRunning: !this.state.isRunning }, () => {
            this.state.isRunning ? this.startTimer() : clearInterval(this.timer)
        });
    }
    lap = () => {
        const { lapTimes, timeElapsed } = this.state;
        this.setState({ lapTimes: lapTimes.concat(timeElapsed) });
    }
    reset = () => {
        clearInterval(this.timer);
        this.setState(this.initialState);
    }
    startTimer () {
        this.startTime = Date.now();
        this.timer = setInterval(this.update, 10);
    }
    update = () => {
        const delta = Date.now() - this.startTime;
        this.setState({ timeElapsed: this.state.timeElapsed + delta });
        this.startTime = Date.now();
    }
    render() {
        const { isRunning, lapTimes, timeElapsed } = this.state;
        return (
            <div>
                <h1>React Stopwatch</h1>
                <TimeElapsed id="timer" timeElapsed={timeElapsed} />
                <button onClick={this.toggle}>
                    {isRunning ? 'Stop' : 'Start'}
                </button>
                <button
                    onClick={isRunning ? this.lap : this.reset}
                    disabled={!isRunning && !timeElapsed}
                >
                    {isRunning || !timeElapsed ? 'Lap' : 'Reset'}
                </button>
                {lapTimes.length > 0 && <LapTimes lapTimes={lapTimes} />}
            </div>
        );
    }
}

const TimeElapsed = (props) => {
    const getUnits = () => {
        const seconds = props.timeElapsed / 1000;
        return {
            min: Math.floor(seconds / 60).toString(),
            sec: Math.floor(seconds % 60).toString(),
            msec: (seconds % 1).toFixed(3).substring(2)
        };
    }
        const leftPad = (width, n) => {
            if ((n + '').length > width) {
                return n;
            }
            const padding = new Array(width).join('0');
            return (padding + n).slice(-width);
        };
        const units = getUnits();
        return (
            <div id={props.id}>
                <span>{leftPad(2, units.min)}:</span>
                <span>{leftPad(2, units.sec)}.</span>
                <span>{units.msec}</span>
            </div>
        );
}

const LapTimes = (props) => {
        const rows = props.lapTimes.map((lapTime, index) =>
            <tr key={++index}>
                <td>{index}</td>
                <td><TimeElapsed timeElapsed={lapTime} /></td>
            </tr>
        );
        return (
            <table id="lap-times">
                <thead>
                    <tr>
                        <th>Lap</th>
                        <th>Time</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
}
Previous1.2 - Clock AppNext1.4 - Challenge

Last updated 7 years ago

For more information, see .

here
Timer Challenge