Creating a simple countdown timer in React

I recently had a scenario come up where the client I’m working for wanted a countdown timer on their homepage.  I had previously built a similar feature in their old website but did not reproduce it when we built the new site. The old website was a hybrid ASP.NET MVC + Razor views + jQuery + angularJS. 

Sample countdown timer

I pulled up my old code assuming I had implemented it with angularJS but much to my surprise I found jQuery code instead.  While the overall premise was good it wasn’t quite going to work without some massaging on the new site.

In order to break our time into the components per that image above we need to get the total seconds left until the event and then further narrow into the time components we want.  Here’s how I did it last time around:

// first we need to determine the time units as seconds:
var daysInSeconds = 24 * 60 * 60, // 86400 seconds
	hoursInSeconds = 60 * 60, // 3600 seconds
	minutesInSeconds = 60,
	maxTimeLeft = 0;

var d, h, m, s;

// Number of days left
d = Math.floor(left / daysInSeconds);
left -= d * daysInSeconds;

// Number of hours left
h = Math.floor(left / hoursInSeconds);
left -= h * hoursInSeconds;

// Number of minutes left
m = Math.floor(left / minutesInSeconds);
left -= m * minutesInSeconds;

// Number of seconds left
s = left;

That worked just fine the previous time around but it doesn’t really give me a lot by way of options.  I decided to look around for an NPM package to provide me this functionality.  Here’s one: countdown. Uncompressed it is 28kb and minified around 13kb.  I’ll be the first to admit that’s too big for the use-case I’m introducing but I’m going with it anyway.  I selected this one because it is highly configurable which, given the client, means I’ll save a lot of time in the future when they change things on me again.

Creating the countdown timer in React

First off let me preface this that I’m using Bootstrap 4 with Font Awesome 5 so please allow that to explain the large className lists.  I tossed back and forth whether I should simplify them for sake of the demo or leave them complete in order to show how I implemented it.  Ultimately I decided to err on the side of completeness.

To get this party started we need a way to display each time component (eg. day, hour, minute, second).  In my own version I called this a TimerSpanDisplay and implemented it as a functional component:

const TimerSpanDisplay = function TimerSpanDisplay(props) {
    const { className, value, label } = props;

    return (
        <div className={`timespan-display d-flex flex-column justify-content-center align-items-center px-2 px-sm-4 px-md-3 px-lg-3 px-xl-3 py-2 ${className || ''}`}>
            <span className="timespan-value">{`${value.toString().padStart(2, '0')}`}</span>
            <span className="timespan-label text-uppercase">{label}</span>
        </div>
    );
}
/* SCSS */
.timespan-display {
	.timespan-value {
		font-weight: bold;
		font-size: 1.25rem;
	}

	.timespan-label {
		font-size: 0.6875rem;
	}
}

Next on our list is the actual timer container for all of these TimerSpanDisplay components.  I decided to call this CountdownTimer.  I’m pretty creative.  My own version required a JavaScript Date object for endDate and then converted it to the client’s timezone (not end-user) for calculation and display purposes.

export class CountdownTimer extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            timespan: this.calculateTimespan(props.endDate)
        };
    }

    componentDidMount() {
        this._timerId = setInterval(() => {
            this.setState({ timespan: this.calculateTimespan(this.props.endDate) });
        }, 1000);
    }

    componentWillUnmount() {
        if (!isNullOrUndefined(this._timerId))
            clearInterval(this._timerId);
    }

    calculateTimespan(endDate) {
        let now = new Date();

        let timespan = countdown(now, endDate, countdown.DAYS | countdown.HOURS | countdown.MINUTES | countdown.SECONDS);
        return timespan;
    }

    render() {
        const { timespan } = this.state;

        return (
            <div className={`bg-dark text-white d-flex justify-content-between w-100`}>
                <TimerSpanDisplay className="ml-5 ml-md-0" value={timespan.days} label="Days" />
                <TimerSpanDisplay value={timespan.hours} label="Hrs" />
                <TimerSpanDisplay value={timespan.minutes} label="Min" />
                <TimerSpanDisplay value={timespan.seconds} label="Sec" />
                <div className="bg-black px-4 px-xl-3 py-1 d-flex align-items-center justify-content-center w-md-100 w-md-auto">
                    <i className="fal fa-angle-right fa-3x"></i>
                </div>
            </div>
        );
    }
}

Now it should be fairly simple to hook this up:

const endDate = new Date(2018, 11, 22);

<CountdownTimer endDate={endDate} />;

Wrap-up

I’ll acknowledge that the countdown NPM package is probably overkill for this particular exercise but again, it saved me the hassle.  You could definitely take this as a springboard and make it more extensible, perhaps add the ability to select which time components to include for example.

Code for this post can be located as a gist on my Github account here.

Credits

Cover Photo by Aron on Unsplash