DEV Community

Cover image for Creating a React Calendar Component: Part 2
Elbert Bae
Elbert Bae

Posted on • Edited on

Creating a React Calendar Component: Part 2

Looking at a new framework in web development can be daunting. Technology moves quickly in this industry and learning new skills is a necessity. Thankfully, new skills don’t mean the we need to learn a new framework every time we start a new project. Diving deeper into an existing one or even revisiting it after some time to stay up to date with new changes are just as important.

In part one of this series, we explored the logic behind creating the dates for us to display on our React calendar component. Now, we will dive into React itself and how the calendar component takes shape in the world of JSX using the function getDatesInMonthDisplay we created in part 1. If you are not familiar with how we created this function and want to know, check out part one of the series.

Before we begin, I will not be doing a deep dive into how React works with the assumption that you, my readers, understand the basic concepts of React’s rendering engine and how it is controlled by state and props . If you are not familiar with React, I recommend creating a simple project to understand the fundamental concepts with their official get started page. Otherwise, read on!

Complete calendar component

Here is the completed React component and today, we will be pulling this apart into 4 parts.

  1. Calendar header component
  2. Weekday indicator component
  3. Date indicator component
  4. Month indicator component

Throughout each part, we will spend time on state management as we discuss the reasons behind why

Section 1: Calendar header

React is most commonly used in single page applications and if coded correctly, can be incredibly efficient in re-rendering parts of the web or application as data changes. This is done through something called state whereby the code looks for changes in data for defined states we want to track.

Since the calendar component wants to display one month at a time, let’s get it to show the month of the date the user selects.

import React, { useState } from 'react';  
import moment from 'moment'  
import './bae-calendar.scss';

const BaeCalendar = () => {  
  const [selectDate, setSelectDate] = 
  useState(moment().toDate());

  return (  
    <div className="bae-calendar-container">  
      Hello World  
    </div>  
  );  
};

export default BaeCalendar;
Enter fullscreen mode Exit fullscreen mode

Using React’s useState hook, we create a state called selectDate like this and set an initial state by using MomentJs to call moment().toDate() to get today’s date object (e.g. 2020–07–08T00:00:00.000Z ).

...  
const [selectDate, setSelectDate] = useState(moment().toDate());  
...
Enter fullscreen mode Exit fullscreen mode

Now that we have a date object to work with, let’s take a look at our calendar header. The calendar, in my view, has 4 logical components and the header was the easiest place to start. Here is the full component and how the sub-component called CalendarHeader is pulled into the BaeCalendar component which will be the root file index.js of the folder.

import React, { useState } from 'react';  
import moment from 'moment'  
import './bae-calendar.scss';

import CalendarHeader from './components/calendar-header';

const BaeCalendar = () => {  
  const [selectDate, setSelectDate] = 
  useState(moment().toDate());

  return (  
    <div className={`bae-calendar-container ${themes[theme]}`}>  
      <CalendarHeader selectDate={selectDate}/>  
    </div>  
  );  
};

export default BaeCalendar;
Enter fullscreen mode Exit fullscreen mode

Here is how the  raw `CalendarHeader` endraw  looks on it’s own!
Here is how the CalendarHeader looks on it’s own!

Let’s take a look at the header component file that utilizes MomentJs to format the date object into what we need. Simple right? MomentJs’s formatting capabilities are top knotch and if you want to learn more, check out the documentation on what the MMMM do and dddd do in their official documentation.

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

const CalendarHeader = ({ selectDate }) => {  
  return (  
    <div className="bae-calendar-header">  
      <div className="left-container">  
        <h1>{moment(selectDate).format('dddd')}</h1>  
        <h1>{moment(selectDate).format('MMMM Do')}</h1>  
      </div>  
      <div className="right-container">  
        <h3>{moment(selectDate).year()}</h3>  
      </div>  
    </div>  
  );  
};

export default CalendarHeader;
Enter fullscreen mode Exit fullscreen mode

You’ll also notice here that somehow, our CalendarHeader component has access to a state we created in the main BaeCalendar parent component. This is done by passing in what we call props . Here is how it looks in the main component as it passes in the props:

<CalendarHeader selectDate={selectDate}/>
Enter fullscreen mode Exit fullscreen mode

And accessed in the CalendarHeader component:

const CalendarHeader = ({ selectDate }) => {  
  ...  
}
Enter fullscreen mode Exit fullscreen mode

Now this component has access to this data! Props can be anything and does not have to be strictly state data, so get creative. If you’re still not sure how props work, check out React’s official get started page and create a small project to play around.

Now.. this is a great start, but there’s something we can improve. We are going to be doing a lot of formatting throughout the calendar component and duplicate code is bad. So, let’s take a moment here and create a utility file called moment-utils.js which will handle the formatting for us. Below is all of the various formats we’ll end up using in our component and we will use this moving forward.

import moment from 'moment';

export const getSpecificDate = (month, dayOfMonth, year) => {  
  return moment(`${month}-${dayOfMonth}-${year}`, 'MM-DD-YYYY').toDate();  
};

export const getDayOfMonth = (date) => moment(date).date();

export const getMonth = (date) => moment(date).month();

export const getYear = (date) => moment(date).year();

export const getToday = () => moment().toDate();

export const getReadableWeekday = (date) => moment(date).format('dddd');

export const getReadableMonthDate = (date) => moment(date).format('MMMM Do');

export const getMonthDayYear = (date) => moment(date).format('MM-DD-YYYY');
Enter fullscreen mode Exit fullscreen mode

So our CalendarHeader will now look like this.

import React from 'react';  
import {  
  getReadableMonthDate,  
  getReadableWeekday,  
  getYear,  
} from '../utils/moment-utils';

const CalendarHeader = ({ selectDate }) => {  
  return (  
    <div className="bae-calendar-header">  
      <div className="left-container">  
        <h1>{getReadableWeekday(selectDate)}</h1>  
        <h1>{getReadableMonthDate(selectDate)}</h1>  
      </div>  
      <div className="right-container">  
        <h3>{getYear(selectDate)}</h3>  
      </div>  
    </div>  
  );  
};

export default CalendarHeader;
Enter fullscreen mode Exit fullscreen mode

Section 2: Weekday indicator component

Now the next section we’ll tackle is the weekday indicator showing the [Sunday — Saturday] representation in our component.

import React, { useState } from 'react';  
import { getToday } from './utils/moment-utils';  
import './bae-calendar.scss';

import CalendarHeader from './components/calendar-header';  
import WeekdayIndicator from './components/weekday-indicator';

const BaeCalendar = () => {  
  const [selectDate, setSelectDate] = useState(moment().toDate());  
  return (  
    <div className={`bae-calendar-container ${themes[theme]}`}>  
      <CalendarHeader selectDate={selectDate}/>  
      <WeekdayIndicator />  
    </div>  
  );  
};  

export default BaeCalendar;
Enter fullscreen mode Exit fullscreen mode

The WeekdayIndicator is quite simple. For all intents and purposes, we don’t actually need to pass any state or props to it. In fact, it’s responsibility is singular which is to display the days of the week.

import React from 'react';

const WeekdayIndicator = () => {  
  return (  
    <div className="bae-weekday-indicators">  
      <div className="weekday-indicator-icon">  
        Sun  
      </div>  
      <div className="weekday-indicator-icon">  
        Mon  
      </div>  
      <div className="weekday-indicator-icon">  
        Tue  
      </div>  
      <div className="weekday-indicator-icon">  
        Wed  
      </div>  
      <div className="weekday-indicator-icon">  
        Thu  
      </div>  
      <div className="weekday-indicator-icon">  
        Fri  
      </div>  
      <div className="weekday-indicator-icon">  
        Sat  
      </div>  
    </div>;  
  )  
};

export default WeekdayIndicator;
Enter fullscreen mode Exit fullscreen mode

Technically this works, but what a pain typing it out! Let’s re-do this in the “Ways of React”.

import React from 'react';

const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

const WeekdayIndicator = () => {  
  const weekdayIcons = weekdays.map((day, key) => {  
    return (  
      <div className="weekday-indicator-icon" key={key}>  
        {day}  
      </div>  
    );  
  });  
  return <div className="bae-weekday-indicators">{weekdayIcons}</div>;  
};

export default WeekdayIndicator;
Enter fullscreen mode Exit fullscreen mode

First, by creating an array of the weekdays, we can utilise JavaScript's .map iterator method to create React JSX code. Since .map returns a new array, this new array assigned to the variable weekdayIcons which returns the following per iteration…

<div className="weekday-indicator-icon" key={key}>  
  {day}  
</div>
Enter fullscreen mode Exit fullscreen mode

You’ll notice a few things here. Why does every element have a key prop passed into it and what is {...} doing?

When creating multiple elements using a loop, React expects you to pass a key prop of unique values. Otherwise, it will complain with a warning which is always annoying to see in our web consoles. As for the curly braces, React automatically assumes any data put inside of it can be a variable. Of course, you can pass in a string value, but that defeats the purpose of its use.

This allows us to pass the weekdayIconsinto the wrapping div element to have the same result as typing out each element individually like this.

...  
return <div className="bae-weekday-indicators">{weekdayIcons}</div>  
...
Enter fullscreen mode Exit fullscreen mode

Here’s where we are at now!
Here’s where we are at now!

Section 3: Date indicator

Thankfully in the previous post (calendar date display logic), we did the bulk of the work to create the date indicators.

import React, { useState } from 'react';  
import { getToday } from './utils/moment-utils';  
import './bae-calendar.scss';

import CalendarHeader from './components/calendar-header';  
import WeekdayIndicator from './components/weekday-indicator';  
import DateIndicator from './components/date-indicator';

const BaeCalendar = () => {  
  const [selectDate, setSelectDate] = useState(moment().toDate());  
  return (  
    <div className={`bae-calendar-container ${themes[theme]}`}>  
      <CalendarHeader selectDate={selectDate}/>  
      <WeekdayIndicator />  
      <DateIndicator  
        selectDate={selectDate}  
        setSelectDate={setSelectDate}  
      />  
    </div>  
  );  
};  

export default BaeCalendar;
Enter fullscreen mode Exit fullscreen mode

You’ll notice we’re passing in two props to the DateIndicator component, but for this part of the series, ignore the second one called setSelectDate and focus on how we will useselectDate. Let’s take a look!

import React from 'react';  
import {  
  getDayOfMonth,  
  getMonthDayYear,  
  getMonth,  
  getYear,  
} from '../utils/moment-utils';  
import { getDatesInMonthDisplay } from '../utils/date-utils';

const DateIndicator = ({ selectDate, setSelectDate }) => {  
  const datesInMonth = getDatesInMonthDisplay(  
    getMonth(selectDate) + 1,  
    getYear(selectDate)  
  );

  const monthDates = datesInMonth.map((i, key) => {  
    return (  
      <div  
        className="date-icon"}  
        data-active-month={i.currentMonth}  
        data-date={i.date.toString()}  
        key={key}  
        onClick={changeDate}  
      >  
        {getDayOfMonth(i.date)}  
      </div>  
    );  
  });

return <div className="bae-date-indicator">{monthDates}</div>;  
};

export default DateIndicator;
Enter fullscreen mode Exit fullscreen mode

By utilizing MomentJs and the helper functions getMonth and getYear , we can get an array of objects with properties date and currentMonth using the selectDate prop! So whichever date the selectDate represents, the DateIndicator is able to use getDatesInMonthDisplay to pull every single date in any month and year.

First, aren’t you glad that we already went through the logic of determining how many dates in the month we need for the display in the getDatesInMonthDisplay function before?

Similar to how we created each day of the week in the WeekIndicator component, we utilize the .map iterator here as well. Rightfully so, because if we had to type this out 42 times… well let me go get some beer first.

const monthDates = datesInMonth.map((i, key) => {  
  return (  
    <div  
      className="date-icon"  
      data-active-month={i.currentMonth}  
      data-date={i.date.toString()}  
      key={key}  
    >  
      {getDayOfMonth(i.date)}  
    </div>  
 );  
});
Enter fullscreen mode Exit fullscreen mode

Let’s break down how we are utilising each item in the array which represents an object with the properties date (date object) and currentMonth (boolean).

First, the div element has a inner content using getDayOfMonth(i.date) which is making use of moment(_date_).date() returning the numerical day of the month. If we didn’t do this and simply passed i.date.toString() (.toString() because we can’t pass a date object into HTML)… well, here’s the chaos that would have any UX/UI designer screaming at you.

Ew…
Ew…

However, this date object is incredibly useful even if it is not friendly to see on the UI of the component which is why we pass it into the data-attribute called data-date as a string. Here is how the element looks in the web console.

Web console view

Simply by using vanilla Javascript, we could do something to have access to the date object of a specific element which we will utilise later on like this.

document.querySelector('.selected[data-date]').getAttribute('data-date')  
// Fri Jul 10 2020 00:00:00 GMT-0700 (Pacific Daylight Time)
Enter fullscreen mode Exit fullscreen mode

Finally, data-active-month={i.currentMonth} provides a "true" or "false" to the data attribute. Can you guess what it’s used for? If you are not sure, make sure you follow up for the third part of this series where I will discuss it further.

Given where we are now, we have enough to make our component interactive. As you can see in a few of the photos, there is a circle highlight that represents the selected date by a user. Let’s see how that works with the useState React hook called setSelectDate .

import React from 'react';  
import {  
  getDayOfMonth,  
  getMonthDayYear,  
  getMonth,  
  getYear,  
} from '../utils/moment-utils';  
import { getDatesInMonthDisplay } from '../utils/date-utils';

const DateIndicator = ({ activeDates, selectDate, setSelectDate }) => {

  // EVENT HANDLING CALLBACK  
  const changeDate = (e) => {  
    setSelectDate(e.target.getAttribute('data-date'));  
  };

  const datesInMonth = getDatesInMonthDisplay(  
    getMonth(selectDate) + 1,  
    getYear(selectDate)  
  );

  const monthDates = datesInMonth.map((i, key) => {  
    const selected =  
      getMonthDayYear(selectDate) === getMonthDayYear(i.date) ? 'selected' : '';  
    const active =  
      activeDates && activeDates[getMonthDayYear(i.date)] ? 'active' : '';

    return (  
      <div  
        className={`date-icon ${selected} ${active}`}  
        data-active-month={i.currentMonth}  
        data-date={i.date.toString()}  
        key={key}

        // EVENT HANDLER  
        onClick={changeDate}  
      >  
        {getDayOfMonth(i.date)}  
      </div>  
    );  
  });

  return <div className="bae-date-indicator">{monthDates}</div>;  
};

export default DateIndicator;
Enter fullscreen mode Exit fullscreen mode

Taking a look at the code above, find setSelectDate and you will notice that it is used inside of a function called changeDate . Javascript by nature is a browser language and event handling is its specialty. If you are not familiar with events in Javascript, read about it in MDN, it is the bread and butter of the browser language.

Following where changeDate is used, you’ll notice that each date-icon element has a prop called onClick that passes in the changeDate as a callback function. This means that when any of the date-icon elements are clicked, it will trigger the function setting off the setSelectDate . The value it passes as the argument to setSelectDate utilises what I showcased above using the data attribute data-date .

The code below responds to the click event which is represented by e . By accessing the target and the data-date attribute, we can grab the new date we want to select and change the state called selectDate .

(e) => e.target.getAttribute('data-date')
Enter fullscreen mode Exit fullscreen mode

By now, you can change the function changeDate to the following to see the new selected date be console logged into the web console, but since you have not yet applied any styling, you won’t see the changes in the icon. However, since the state is still changing, you should see the CalendarHeader component’s data update as it re-renders any components utilising the state selectDate !

const changeDate = (e) => {  
  console.log(e.target.getAttribute('data-date');  
  setSelectDate(e.target.getAttribute('data-date'));  
}
Enter fullscreen mode Exit fullscreen mode

Almost there… Section 4: Month indicators

By now, you should have a functioning calendar component that can change the CalendarHeader data with new selected dates and even change the month’s display by clicking one of the overflow dates. Let’s wrap up part 2 of this series by adding in the MonthIndicator component!

import React, { useState } from 'react';  
import { getToday } from './utils/moment-utils';  
import './bae-calendar.scss';  
import CalendarHeader from './components/calendar-header';  
import WeekdayIndicator from './components/weekday-indicator';  
import DateIndicator from './components/date-indicator';  
import MonthIndicator from './components/month-indicator';

const BaeCalendar = () => {  
  const [selectDate, setSelectDate] = useState(moment().toDate());  
  return (  
    <div className={`bae-calendar-container ${themes[theme]}`}>  
      <CalendarHeader selectDate={selectDate}/>  
      <WeekdayIndicator />  
      <DateIndicator  
        selectDate={selectDate}  
        setSelectDate={setSelectDate}  
      />  
      <MonthIndicator   
        selectDate={selectDate}   
        setSelectDate={setSelectDate}  
      />  
    </div>  
  );  
};  

export default BaeCalendar;
Enter fullscreen mode Exit fullscreen mode

Last sub-component to do, let’s get in there take a look at how it’s constructed.

import React from 'react';  
import { getMonth } from '../utils/moment-utils';  
import { getMonthSet } from '../utils/date-utils';  
import './month-indicator.scss';

import { monthsFull } from '../constants/dates';

const MonthIndicator = ({ selectDate, setSelectDate }) => {  
  const changeMonth = (e) => {  
    setSelectDate(e.target.getAttribute('data-date'));  
  };

  const monthSet = getMonthSet(selectDate);

  return (  
    <div className="bae-month-indicator">  
      <h4 data-date={monthSet.prev} onClick={changeMonth}>  
        {monthsFull[getMonth(monthSet.prev)]}  
      </h4>  
      <h3>{monthsFull[getMonth(monthSet.current)]}</h3>  
      <h4 data-date={monthSet.next} onClick={changeMonth}>  
        {monthsFull[getMonth(monthSet.next)]}  
      </h4>  
    </div>  
  );  
};

export default MonthIndicator;
Enter fullscreen mode Exit fullscreen mode

We see two props again here (selectDate and setSelectDate ). By now, it’s clear why we need selectDate . Using the current selected date, we can pull out the current, previous, and following month. Can you think of any challenges we might have determining the previous and following months based on the current one?

Two months immediately come to mind which are December and January . By design, we want these elements to be clickable to change the month on display. If we only took the current month and used moment to subtract or add a month, it obviously would not work for all cases. Going from January to December means that the year changes with the same logic applied in reverse.

So… let’s create a little helper function to handle this for us!

const getMonthSet = (selectDate) => {  
  const month = getMonth(selectDate) + 1;  
  const result = {  
    current: selectDate,  
    prev: getSpecificDate(month - 1, 1, getYear(selectDate)),  
    next: getSpecificDate(month + 1, 1, getYear(selectDate)),  
  };

  if (month === 1) {  
    result.prev = getSpecificDate(12, 1, getYear(selectDate) - 1);  
  }

  if (month === 12) {  
    result.next = getSpecificDate(1, 1, getYear(selectDate) + 1);  
  }

  return result;  
};
Enter fullscreen mode Exit fullscreen mode

Straightforward right? By getting the month of the currently selected date (+1 since months return in indexed form), we can use MomentJs to construct the prev and next month’s date objects. If the month is 1 for January, we will grab the year and subtract one. If the month is 12 for December, do the opposite and add one.

Similar to the date-icons in the DateIndicator component, this one adds the data-attribute data-date to the previous and following month elements.

...  
<div className="bae-month-indicator">  
  <h4 data-date={monthSet.prev} onClick={changeMonth}>  
    {monthsFull[getMonth(monthSet.prev)]}  
  </h4>  
  <h3>{monthsFull[getMonth(monthSet.current)]}</h3>  
  <h4 data-date={monthSet.next} onClick={changeMonth}>  
    {monthsFull[getMonth(monthSet.next)]}  
  </h4>  
</div>  
...
Enter fullscreen mode Exit fullscreen mode

As you can see, these two elements also appear to have a onClick event listener calling the function changeMonth . Simlar to the callback function in the DateIndicator , it’s changing the state selectDate by calling setSelectDate .

Bit problematic though. The name changeMonth seems a bit misleading, because we’re technically changing the whole date of the selectDate state and this code is duplicated! Moments like these are where you should consider refactoring this to reduce duplicated code and change names of functions to be more accurate with its intended behavior.

For now, let’s change the name to changeDate and leave it in the component. In cases like these, there are many opinions on whether to refactor the duplicate code. However, for a small project, I prefer to keep callback functions in the component where they are used. This is something that should be reconsidered as a project gets larger over time, but this should be fine for now.

Component with Styling
Component with Styling

Not bad right? By now you should have a functioning React calendar component that changes dates in the CalendarHeader and MonthIndicator as you click the dates.

If you want to take a look at the code for the entire component, take a look at the Github repository.

In the last and final part of this series, we will add some features to the component that makes it usable for others, show selected dates, as well as the styling. Some concepts we will touch upon is component re-usability, style sheet organisation, and general CSS/SASS tricks using Flex and Grid.

Hope you enjoyed reading and found it useful to inspire you to continue developing your skills with fun mini-projects!

Top comments (0)