Learning Moments 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!
Here is the completed React component and today, we will be pulling this apart into 4 parts.
- Calendar header component
- Weekday indicator component
- Date indicator component
- 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;
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());
...
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;
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;
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}/>
And accessed in the CalendarHeader
component:
const CalendarHeader = ({ selectDate }) => {
...
}
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');
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;
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;
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;
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;
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>
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 weekdayIcons
into 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>
...
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;
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;
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>
);
});
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.
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.
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)
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;
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')
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'));
}
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;
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;
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;
};
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>
...
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.
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!