How to Create a Simple React Calendar with Styled Component
Zhiyue Yi
Posted on January 5, 2020
Visit my Blog for the original post: How to Create a Simple React Calendar with Styled Component
I found it quite interesting doing small components which are widely used in web developments. When I was a junior web developer, I tended to search libraries or plugins online if I need to build some featured. It could be a hard time to implement it because I didn't try to think how it actually works and I had to rely on the poorly written documents. And sometimes, customization was also difficult because it was hard to understand why the author did the plugin in their way.
The calendar was one of the most common examples. There are a lot of plugins online, but few of them really teach you how it works. When I was in my previous company as a junior developer, I was tasked to customize a calendar with integration of some business requirements, none of the libraries I found online fulfilled my needs. Then I realize, hey, why not build my own calendar from scratch?
It's not hard. Let's do it with React and Styled Component!
Solution
The final implementation can be found at simple-react-calendar if you wish to implement quickly without reading through my explanation.
import * as React from 'react';
import { useState, useEffect } from 'react';
import styled, { css } from 'styled-components';
const Frame = styled.div`
width: 400px;
border: 1px solid lightgrey;
box-shadow: 2px 2px 2px #eee;
`;
const Header = styled.div`
font-size: 18px;
font-weight: bold;
padding: 10px 10px 5px 10px;
display: flex;
justify-content: space-between;
background-color: #f5f6fa;
`;
const Button = styled.div`
cursor: pointer;
`;
const Body = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
`;
const Day = styled.div`
width: 14.2%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
${props =>
props.isToday &&
css`
border: 1px solid #eee;
`}
${props =>
props.isSelected &&
css`
background-color: #eee;
`}
`;
export function Calendar() {
const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_OF_THE_WEEK = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
const MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
const today = new Date();
const [date, setDate] = useState(today);
const [day, setDay] = useState(date.getDate());
const [month, setMonth] = useState(date.getMonth());
const [year, setYear] = useState(date.getFullYear());
const [startDay, setStartDay] = useState(getStartDayOfMonth(date));
useEffect(() => {
setDay(date.getDate());
setMonth(date.getMonth());
setYear(date.getFullYear());
setStartDay(getStartDayOfMonth(date));
}, [date]);
function getStartDayOfMonth(date: Date) {
return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
}
function isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
const days = isLeapYear(date.getFullYear()) ? DAYS_LEAP : DAYS;
return (
<Frame>
<Header>
<Button onClick={() => setDate(new Date(year, month - 1, day))}>Prev</Button>
<div>
{MONTHS[month]} {year}
</div>
<Button onClick={() => setDate(new Date(year, month + 1, day))}>Next</Button>
</Header>
<Body>
{DAYS_OF_THE_WEEK.map(d => (
<Day key={d}>
<strong>{d}</strong>
</Day>
))}
{Array(days[month] + (startDay - 1))
.fill(null)
.map((_, index) => {
const d = index - (startDay - 2);
return (
<Day
key={index}
isToday={d === today.getDate()}
isSelected={d === day}
onClick={() => setDate(new Date(year, month, d))}
>
{d > 0 ? d : ''}
</Day>
);
})}
</Body>
</Frame>
);
}
Explanation
Initialize Calendar Component
Initialization of the component is rather simple. Firstly, import the necessary libraries, and then, create a function component called Calendar
.
Inside of the component, let's return an empty div
for now and add in some constants which are
-
DAYS
: an array of numbers of days in every month for a normal year -
DAYS_LEAP
: an array of numbers of days in every month for a leap year -
DAYS_OF_THE_WEEK
: an array of names of days of the week -
MONTHS
: an array of names of months
import * as React from 'react';
import { useState, useEffect } from 'react';
import styled, { css } from 'styled-components';
export function Calendar() {
const DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const DAYS_OF_THE_WEEK = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN'];
const MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
// Will be implemented below
return (
<div />
);
}
Identify Component Layout
Now let's decide what the layout of a calendar component is. Since we are building a basic calendar component, we only need a header with a title current month and year, a previous month button and a next month button.
As for the body part, it is consisted of 2 parts, which are one row of days of the week and multiple rows of actual days.
Now, let's create these parts using styled components and put them above the calendar function component.
const Frame = styled.div`
width: 400px;
border: 1px solid lightgrey;
box-shadow: 2px 2px 2px #eee;
`;
const Header = styled.div`
font-size: 18px;
font-weight: bold;
padding: 10px 10px 5px 10px;
display: flex;
justify-content: space-between;
background-color: #f5f6fa;
`;
const Button = styled.div`
cursor: pointer;
`;
const Body = styled.div`
width: 100%;
display: flex;
flex-wrap: wrap;
`;
const Day = styled.div`
width: 14.2%;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
${props =>
props.isToday &&
css`
border: 1px solid #eee;
`}
${props =>
props.isSelected &&
css`
background-color: #eee;
`}
`;
Notice that:
- I use
14.2%
as the width of theDay
component, because there can only be 7 days in a week / row and100% / 7
is approximately14.2%
. - For
Day
styled component, I will check 2 propsisToday
andisSelected
to show a grey border if the day is today, or a grey background if it is selected
Use React Hooks to Manage Date / Month / Year as States
A calendar must have a current day, month and year displayed. They are considered as states to the component. Hence, we use useState
react hook to manage these states. The initial values of them are generated from today's date by default (You may also make the default value of date
come from a prop of this component for further extensibility).
Besides current day, month and year, you also need startDay
to identify the first day of the month is which day of the week (Monday, Tuesday or others). After you know which day it is, it's much easier for you to identify the positions of all the days in the calendar.
After creating all the states, we also need to manage updates of them. We should make date
variable as an entry point for calculations of day
, month
, year
and startDay
. Therefore, we can use useEffect
react hook to update day
, month
, year
and startDay
with a dependency of date
, so that later, when we click any day in the calendar, we can call setDate()
to update date
and trigger the rest of the states to update too.
const today = new Date();
const [date, setDate] = useState(today);
const [day, setDay] = useState(date.getDate());
const [month, setMonth] = useState(date.getMonth());
const [year, setYear] = useState(date.getFullYear());
const [startDay, setStartDay] = useState(calculateStartDayOfMonth(date));
useEffect(() => {
setDay(date.getDate());
setMonth(date.getMonth());
setYear(date.getFullYear());
setStartDay(calculateStartDayOfMonth(date));
}, [date]);
Get Start Day of the Month
As mentioned above, we need to get the start day of the month, which is rather simple and straightforward.
function getStartDayOfMonth(date: Date) {
return new Date(date.getFullYear(), date.getMonth(), 1).getDay();
}
Check If It's in a Leap Year
We also needs to check if we are currently in a leap year, so that we can show correct number of days in February.
I extracted a picture from Wikipedia for better illustration of determination of a leap year.
It's quite clear that, if a year is a leap year, the year is divisible by 4 and by 400 but not by 100.
For example,
- 2020 is a leap year because it is divisible by 4
- 2010 is not a leap year because it is not divisible by 4
- 2000 is a leap year because it is divisible by 400
- 1900 is not a leap year. Although 1900 is divisible by 4 but it is also divisible by 100
(It's better to write a unit test for it!!)
function isLeapYear(year: number) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
Build the Calendar with TSX!
Finally, we can complete the component by adding the render part.
The 2 buttons in the headers trigger setDate()
when being clicked. And it will then trigger useEffect()
callback and then update day
, month
, year
and startDay
, where month
and day
are displayed in the title of the header, day
is used to determine if the day is the current selected day and start day to compute how many empty blocks it should have before the 1st day of the month.
const days = isLeapYear(date.getFullYear()) ? DAYS_LEAP : DAYS;
return (
<Frame>
<Header>
<Button onClick={() => setDate(new Date(year, month - 1, day))}>Prev</Button>
<div>
{MONTHS[month]} {year}
</div>
<Button onClick={() => setDate(new Date(year, month + 1, day))}>Next</Button>
</Header>
<Body>
{DAYS_OF_THE_WEEK.map(d => (
<Day key={d}>
<strong>{d}</strong>
</Day>
))}
{Array(days[month] + (startDay - 1))
.fill(null)
.map((_, index) => {
const d = index - (startDay - 2);
return (
<Day
key={index}
isToday={d === today.getDate()}
isSelected={d === day}
onClick={() => setDate(new Date(year, month, d))}
>
{d > 0 ? d : ''}
</Day>
);
})}
</Body>
</Frame>
);
Conclusion
Today I shared how to create a simple react calender with styled component. It's not as difficult as imagined because the only critical part, I think, is to know you need to determine what is the day of the week for the first day of the week. If you can do that, you can determine all the positions of the days. The rest works are just grooming your components to make it more appealing.
Thank you for reading!!
Featured image is credited to Bich Tran from Pexels
Posted on January 5, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.