Introduction
Many web applications need to manage dates using a calendar, however the overwhelming majority of articles/tutorials always use third-party libraries. It's not that it's bad, on the contrary, it helps a lot to prototype an application, but if the design and requirements are custom, it's very difficult for the developer.
For that same reason in today's article I will teach you how to create a base component that can later be extended to add even more functionality.
What are we going to use?
Today to be different we are going to use two of my favorite libraries:
- Day.js - is a library that helps us manipulate, parse and validate dates
- Stitches - a css-in-js styling library with phenomenal development experience
Bear in mind that although these are the libraries used in this article, the same result is also easily replicable with others.
Prerequisites
To follow this tutorial, you need:
- Basic understanding of React
- Basic understanding of TypeScript
If you're not familiar with TypeScript, it's okay because you just "ignore" the data-types and the code is exactly the same as in JavaScript.
Getting Started
As a first step, create a project directory and navigate into it:
yarn create vite react-calendar-ts --template react-ts
cd react-calendar-ts
Now we can install the necessary dependencies:
yarn add dayjs react-icons @stitches/react @fontsource/anek-telugu
Then we create the styles of our html elements in a file called styles.ts
:
// @/src/styles.ts
import { styled } from "@stitches/react";
export const MainWrapper = styled("div", {
width: 240,
borderRadius: 10,
padding: 20,
backgroundColor: "white",
boxShadow: "-6px 7px 54px -24px rgba(0,0,0,0.5)",
fontFamily: "Anek Telugu",
});
export const CalendarHeaderWrapper = styled("div", {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
});
export const WeekDaysWrapper = styled("div", {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
});
export const WeekDayCell = styled("div", {
height: 30,
width: 30,
margin: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#9BA4B4",
});
export const CalendarContentWrapper = styled("div", {
display: "flex",
flexDirection: "row",
});
export const CalendarDayCell = styled("div", {
height: 30,
width: 30,
display: 'flex',
alignItems: "center",
justifyContent: "center",
borderRadius: 6,
margin: 2,
variants: {
variant: {
default: {
color: "#1B1B2F",
},
today: {
color: "#E43F5A",
},
nextMonth: {
color: "#DAE1E7",
},
},
},
});
With the styling of the elements created and the variants of the day already added, we can now start working on the App.tsx
component that will contain all the logic of today's article:
// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
import * as Styles from "./styles";
export const App = () => {
// logic goes here...
return (
// JSX goes here...
)
}
In the code above we made the necessary imports to create our component. The next step is to acquire the values of three important things, the current day, the first day of the month and what the first day of the first week of the month is.
// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
import * as Styles from "./styles";
export const App = () => {
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const currentDay = useMemo(() => dayjs().toDate(), []);
const firstDayOfTheMonth = useMemo(
() => selectedDate.clone().startOf("month"),
[selectedDate]
);
const firstDayOfFirstWeekOfMonth = useMemo(
() => dayjs(firstDayOfTheMonth).startOf("week"),
[firstDayOfTheMonth]
);
// more logic goes here...
return (
// JSX goes here...
)
}
With these three dates acquired, we now need to create two functions, one to generate the first day of each week of the month, while the second function will be responsible for generating the week of the month taking into account the first day of the week.
// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
import * as Styles from "./styles";
export const App = () => {
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const currentDay = useMemo(() => dayjs().toDate(), []);
const firstDayOfTheMonth = useMemo(
() => selectedDate.clone().startOf("month"),
[selectedDate]
);
const firstDayOfFirstWeekOfMonth = useMemo(
() => dayjs(firstDayOfTheMonth).startOf("week"),
[firstDayOfTheMonth]
);
const generateFirstDayOfEachWeek = useCallback((day: Dayjs): Dayjs[] => {
const dates: Dayjs[] = [day];
for (let i = 1; i < 6; i++) {
const date = day.clone().add(i, "week");
dates.push(date);
}
return dates;
}, []);
const generateWeek = useCallback((day: Dayjs): Date[] => {
const dates: Date[] = [];
for (let i = 0; i < 7; i++) {
const date = day.clone().add(i, "day").toDate();
dates.push(date);
}
return dates;
}, []);
// more logic goes here...
return (
// JSX goes here...
)
}
Now that we have the necessary dates and the necessary functions to generate the weeks of the month, we now need to use the useMemo()
hook to react to the changes of the dates mentioned above and generate the days and weeks of the month, as well as memoizing the same, as follows:
// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
import * as Styles from "./styles";
export const App = () => {
const [selectedDate, setSelectedDate] = useState<Dayjs>(dayjs());
const currentDay = useMemo(() => dayjs().toDate(), []);
const firstDayOfTheMonth = useMemo(
() => selectedDate.clone().startOf("month"),
[selectedDate]
);
const firstDayOfFirstWeekOfMonth = useMemo(
() => dayjs(firstDayOfTheMonth).startOf("week"),
[firstDayOfTheMonth]
);
const generateFirstDayOfEachWeek = useCallback((day: Dayjs): Dayjs[] => {
const dates: Dayjs[] = [day];
for (let i = 1; i < 6; i++) {
const date = day.clone().add(i, "week");
dates.push(date);
}
return dates;
}, []);
const generateWeek = useCallback((day: Dayjs): Date[] => {
const dates: Date[] = [];
for (let i = 0; i < 7; i++) {
const date = day.clone().add(i, "day").toDate();
dates.push(date);
}
return dates;
}, []);
const generateWeeksOfTheMonth = useMemo((): Date[][] => {
const firstDayOfEachWeek = generateFirstDayOfEachWeek(
firstDayOfFirstWeekOfMonth
);
return firstDayOfEachWeek.map((date) => generateWeek(date));
}, [generateFirstDayOfEachWeek, firstDayOfFirstWeekOfMonth, generateWeek]);
return (
// JSX goes here...
)
}
With the logic finished, we just need to map the data we get in the generateWeeksOfTheMonth
variable, but we also have to take into account that we need to navigate between months and update the selected date. Just like we need to visually identify the current day on the calendar. Which can be done as follows:
// @/src/App.tsx
import "@fontsource/anek-telugu";
import { useCallback, useMemo, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
import * as Styles from "./styles";
export const App = () => {
// hidden for simplicity...
return (
<Styles.MainWrapper>
<Styles.CalendarHeaderWrapper>
<h3>{selectedDate.clone().format("MMM YYYY")}</h3>
<div>
<MdKeyboardArrowLeft
size={25}
onClick={() => setSelectedDate((date) => date.subtract(1, "month"))}
/>
<MdKeyboardArrowRight
size={25}
onClick={() => setSelectedDate((date) => date.add(1, "month"))}
/>
</div>
</Styles.CalendarHeaderWrapper>
<Styles.WeekDaysWrapper>
{generateWeeksOfTheMonth[0].map((day, index) => (
<Styles.WeekDayCell key={`week-day-${index}`}>
{dayjs(day).format("dd")}
</Styles.WeekDayCell>
))}
</Styles.WeekDaysWrapper>
{generateWeeksOfTheMonth.map((week, weekIndex) => (
<Styles.CalendarContentWrapper key={`week-${weekIndex}`}>
{week.map((day, dayIndex) => (
<Styles.CalendarDayCell
key={`day-${dayIndex}`}
variant={
selectedDate.clone().toDate().getMonth() !== day.getMonth()
? "nextMonth"
: dayjs(currentDay).isSame(day, "date")
? "today"
: "default"
}
>
{day.getDate()}
</Styles.CalendarDayCell>
))}
</Styles.CalendarContentWrapper>
))}
</Styles.MainWrapper>
)
}
If you've followed the article so far, I believe you should have a result very similar to this one in your browser:
What are the next challenges?
This point is quite relative, but I would focus on the following features:
- create a component for the calendar that is reusable
- the component must be stateless
- single-day and multi-day selection
- in the selection of multiple days, have a toggle whether to include weekends or not
Conclusion
As usual, I hope you enjoyed the article and that it helped you with an existing project or simply wanted to try it out.
If you found a mistake in the article, please let me know in the comments so I can correct it. Before finishing, if you want to access the source code of this article, I leave here the link to the github repository.
Top comments (1)
Hi, thank you so much for this tutorial, it really helped me out! However, i can't figure out what one would change in this code to make the calendar start from monday. How would you do it?