How to Create a Simple React Calendar with Styled Component
Tags: react, typescript, javascript, tutorial
Published: 2020-01-05 06:54:44

Visit my Blog for the original post: [How to Create a Simple React Calendar with Styled Component](https://yizhiyue.me/2020/01/04/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](https://codesandbox.io/s/simple-react-calendar-48knh) if you wish to implement quickly without reading through my explanation.

```tsx
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

```tsx
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.

![Structure of Calendar Component](https://i.imgur.com/zBjA4c6.png)

Now, let's create these parts using styled components and put them above the calendar function component.

```tsx
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:

1. I use `14.2%` as the width of the `Day` component, because there can only be 7 days in a week / row and `100% / 7` is approximately `14.2%`.
2. For `Day` styled component, I will check 2 props `isToday` and `isSelected` 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.

```tsx
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.

```tsx
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](https://zh.wikipedia.org/wiki/%E9%97%B0%E5%B9%B4)** for better illustration of determination of a leap year.

> ![Leap Year](https://upload.wikimedia.org/wikipedia/commons/1/19/Leap_year.gif)

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!!)

```tsx
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.

```tsx
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](https://www.pexels.com/photo/photo-of-planner-and-writing-materials-760710/)_