DEV Community

Florin Pop
Florin Pop

Posted on • Originally published at florin-pop.com

How to create a Countdown Component with React and MomentJS

Recently I had to create a Countdown for one of my other projects and I thought that it could also make a good tutorial, so in this post we're going to create this component using React and a little bit of SVG. 😄

You can find the final result in this Codepen example:

First we will create the countdown functionality and then we will look into how to create the animated arc using SVG with some fancy functions. 😉

Creating the Countdown functionality

For this we're going to use MomentJS library which will help us: Parse, validate, manipulate, and display dates and times.

Basically what we're going to need is to have 2 dates:

  • the current date or now
  • the final date or then

When we have these 2 dates, we can subtract now from then using moment and we'll get the remaining time (or the countdown value).

For the then date, we'll need to pass 2 strings:

  • one, the timeTillDate string containing the final date until which we want to count (e.g: 05 26 2019, 6:00 am)
  • two, the timeFormat string that is used by moment in order to validate the time format (in our example would be: MM DD YYYY, h:mm a)

You can find out more about parsing strings and the formatting them in the documentation.

Let's see how this looks in code:

import moment from 'moment';

const then = moment(timeTillDate, timeFormat);
const now = moment();
const countdown = moment(then - now);

Note: the timeTillDate, timeFormat values will be provided inside the React component, for now we're using them as examples.

From the countdown object we can get all the values that we want to display in our component - days, hours, minutes and seconds left until we reach the then time.

import moment from 'moment';

const then = moment(timeTillDate, timeFormat);
const now = moment();
const countdown = moment(then - now);
const days = countdown.format('D');
const hours = countdown.format('HH');
const minutes = countdown.format('mm');
const seconds = countdown.format('ss');

Later we'll add this code in a JS interval that would be called every second, but before that let's set up the react component for it.

The Countdown Component

For this we're going to create a class based component as we need access to the state of the component because we'll save these 4 values (days, hours, minutes, seconds) in it. By default these values are undefined.

import React from 'react';

class Countdown extends React.Component {
    state = {
        days: undefined,
        hours: undefined,
        minutes: undefined,
        seconds: undefined
    };

    render() {
        const { days, hours, minutes, seconds } = this.state;

        return (
            <div>
                <h1>Countdown</h1>
                <div className="countdown-wrapper">
                    <div className="countdown-item">
                        {days}
                        <span>days</span>
                    </div>
                    <div className="countdown-item">
                        {hours}
                        <span>hours</span>
                    </div>
                    <div className="countdown-item">
                        {minutes}
                        <span>minutes</span>
                    </div>
                    <div className="countdown-item">
                        {seconds}
                        <span>seconds</span>
                    </div>
                </div>
            </div>
        );
    }
}

Next, let's create the interval that runs every second and saves the values in the state of the component. We'll do this interval inside the componentDidMount lifecycle method and we are going to clear the interval in the componentWillUnmount lifecycle method as we don't want to keep it running after the component is removed from the DOM.

import React from 'react';
import moment from 'moment';

class Countdown extends React.Component {
    state = {
        days: undefined,
        hours: undefined,
        minutes: undefined,
        seconds: undefined
    };

    componentDidMount() {
        this.interval = setInterval(() => {
            const { timeTillDate, timeFormat } = this.props;
            const then = moment(timeTillDate, timeFormat);
            const now = moment();
            const countdown = moment(then - now);
            const days = countdown.format('D');
            const hours = countdown.format('HH');
            const minutes = countdown.format('mm');
            const seconds = countdown.format('ss');

            this.setState({ days, hours, minutes, seconds });
        }, 1000);
    }

    componentWillUnmount() {
        if (this.interval) {
            clearInterval(this.interval);
        }
    }

    render() {
        const { days, hours, minutes, seconds } = this.state;

        return (
            <div>
                <h1>Countdown</h1>
                <div className="countdown-wrapper">
                    <div className="countdown-item">
                        {days}
                        <span>days</span>
                    </div>
                    <div className="countdown-item">
                        {hours}
                        <span>hours</span>
                    </div>
                    <div className="countdown-item">
                        {minutes}
                        <span>minutes</span>
                    </div>
                    <div className="countdown-item">
                        {seconds}
                        <span>seconds</span>
                    </div>
                </div>
            </div>
        );
    }
}

The CSS

We have the countdown functionality all up and running now, let's style it a little bit:

@import url('https://fonts.googleapis.com/css?family=Lato');

* {
    box-sizing: border-box;
}

body {
    font-family: 'Lato', sans-serif;
}

h1 {
    letter-spacing: 2px;
    text-align: center;
    text-transform: uppercase;
}

.countdown-wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
}

.countdown-item {
    color: #111;
    font-size: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    line-height: 30px;
    margin: 10px;
    padding-top: 10px;
    position: relative;
    width: 100px;
    height: 100px;
}

.countdown-item span {
    color: #333;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
}

Nothing fancy in the CSS; we're using flexbox to position the items within the wrapper.

Lastly, let's create the SVG arc that will be surrounding each item in our countdown.

The SVGCircle Component

Before we do that, there are a couple of functions that we need in order to create the customizable SVG arc. I found these on StackOverflow. For more information you should go there and read the detailed explanation of the functions.

function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
    var angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;

    return {
        x: centerX + radius * Math.cos(angleInRadians),
        y: centerY + radius * Math.sin(angleInRadians)
    };
}

function describeArc(x, y, radius, startAngle, endAngle) {
    var start = polarToCartesian(x, y, radius, endAngle);
    var end = polarToCartesian(x, y, radius, startAngle);

    var largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';

    var d = [
        'M',
        start.x,
        start.y,
        'A',
        radius,
        radius,
        0,
        largeArcFlag,
        0,
        end.x,
        end.y
    ].join(' ');

    return d;
}

Basically the above function calculates how the arc should be drawn by providing a set of values as: the start and ending points, the radius and the angles.

Back to our React Component, we're going to create the svg and we'll have a path tag within it which will draw the arc (the d prop) by giving it a radius property. The other 4 values within the describeArc function are fixed as we don't want to modify it and we are customizing it to look good for our example.

const SVGCircle = ({ radius }) => (
    <svg className="countdown-svg">
        <path
            fill="none"
            stroke="#333"
            stroke-width="4"
            d={describeArc(50, 50, 48, 0, radius)}
        />
    </svg>
);

And we also need a little bit of CSS to position it inside the .countdown-item (See where this component goes in the final result section):

.countdown-svg {
    position: absolute;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
}

Before adding this component inside the Countdown component we need to convert the values that we have (days, hours, minutes and seconds) to their corresponding radius values.

For this we'll need another simple function that will map a number within a range (in our case the date values) to another range of numbers (in our case, the radius). This function is also from StackOverflow:

function mapNumber(number, in_min, in_max, out_min, out_max) {
    return (
        ((number - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
    );
}

The final result

Finally, let's add the new SVGCircle component inside each of the .countdown-items and put everything together:

import React from 'react';
import moment from 'moment';

class Countdown extends React.Component {
    state = {
        days: undefined,
        hours: undefined,
        minutes: undefined,
        seconds: undefined
    };

    componentDidMount() {
        this.interval = setInterval(() => {
            const { timeTillDate, timeFormat } = this.props;
            const then = moment(timeTillDate, timeFormat);
            const now = moment();
            const countdown = moment(then - now);
            const days = countdown.format('D');
            const hours = countdown.format('HH');
            const minutes = countdown.format('mm');
            const seconds = countdown.format('ss');

            this.setState({ days, hours, minutes, seconds });
        }, 1000);
    }

    componentWillUnmount() {
        if (this.interval) {
            clearInterval(this.interval);
        }
    }

    render() {
        const { days, hours, minutes, seconds } = this.state;

        // Mapping the date values to radius values
        const daysRadius = mapNumber(days, 30, 0, 0, 360);
        const hoursRadius = mapNumber(hours, 24, 0, 0, 360);
        const minutesRadius = mapNumber(minutes, 60, 0, 0, 360);
        const secondsRadius = mapNumber(seconds, 60, 0, 0, 360);

        if (!seconds) {
            return null;
        }

        return (
            <div>
                <h1>Countdown</h1>
                <div className="countdown-wrapper">
                    {days && (
                        <div className="countdown-item">
                            <SVGCircle radius={daysRadius} />
                            {days}
                            <span>days</span>
                        </div>
                    )}
                    {hours && (
                        <div className="countdown-item">
                            <SVGCircle radius={hoursRadius} />
                            {hours}
                            <span>hours</span>
                        </div>
                    )}
                    {minutes && (
                        <div className="countdown-item">
                            <SVGCircle radius={minutesRadius} />
                            {minutes}
                            <span>minutes</span>
                        </div>
                    )}
                    {seconds && (
                        <div className="countdown-item">
                            <SVGCircle radius={secondsRadius} />
                            {seconds}
                            <span>seconds</span>
                        </div>
                    )}
                </div>
            </div>
        );
    }
}

const SVGCircle = ({ radius }) => (
    <svg className="countdown-svg">
        <path
            fill="none"
            stroke="#333"
            stroke-width="4"
            d={describeArc(50, 50, 48, 0, radius)}
        />
    </svg>
);

// From StackOverflow: https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
    var angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;

    return {
        x: centerX + radius * Math.cos(angleInRadians),
        y: centerY + radius * Math.sin(angleInRadians)
    };
}

function describeArc(x, y, radius, startAngle, endAngle) {
    var start = polarToCartesian(x, y, radius, endAngle);
    var end = polarToCartesian(x, y, radius, startAngle);

    var largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1';

    var d = [
        'M',
        start.x,
        start.y,
        'A',
        radius,
        radius,
        0,
        largeArcFlag,
        0,
        end.x,
        end.y
    ].join(' ');

    return d;
}

// From StackOverflow: https://stackoverflow.com/questions/10756313/javascript-jquery-map-a-range-of-numbers-to-another-range-of-numbers
function mapNumber(number, in_min, in_max, out_min, out_max) {
    return (
        ((number - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
    );
}

All you have to do now to use the Countdown component is to pass it the two props (timeTillDate and timeFormat) and you're golden 😉:

<Countdown timeTillDate="05 26 2019, 6:00 am" timeFormat="MM DD YYYY, h:mm a" />

Conclusion

It was a fun little project with React, wasn't it? 😄

When I built this I learned a little bit more about how to work with the momentjs library and also with svgs to draw an arc.

Let me know if you have any questions regarding this tutorial.

Happy Coding! 😇


Originally posted on www.florin-pop.com - Check it out for more tutorials and coding challenges.

Top comments (6)

Collapse
 
itachiuchiha profile image
Itachi Uchiha

Can I write a post with this information for Vue? I will specify the source for this post.

Thanks. This post is awesome!

Collapse
 
florinpop17 profile image
Florin Pop

Sure. Make sure you specify the original source from my blog and also let me see it when it’s done! ☺️

Collapse
 
itachiuchiha profile image
Itachi Uchiha

I'll :) thanks.

Collapse
 
irreverentmike profile image
Mike Bifulco

Great post as always, Florin. Thank you!

Collapse
 
florinpop17 profile image
Florin Pop

I'm very glad that you liked it! Thank you! :)

Collapse
 
jvinhit profile image
Vinh Nguyen

my store learning react.

jvinhit.github.io/hm-test/