React Big Calendar: Building a Scheduling App Without Any UI Libraries
In this post, you will learn how to build a React big calendar with scheduling without any additional libraries.
This is how it will look. We have a calendar where we can jump between days, switch months, jump to today, and select a date to check what meetings are planned for that day.
We won't use any UI libraries to create this project, but we will need a library to work with dates in JavaScript.
The default date handling in JavaScript is really painful and not flexible.
A library called Luxon is a popular solution for working with dates in a comfortable and efficient manner.
DateTime.now().setZone('America/New_York').minus({weeks:1}).endOf('day').toISO();
We can write our code like this. Even if you've never worked with Luxon, you can understand what we are doing here. First, we get the current date. Then, we set the timezone, subtract one week from this date, and get the end of the day. The code is extremely readable, easy to support, and it will help us a lot in creating an interactive calendar with React.
Initial Project
Here, I have already prepared an empty project for us.
import Calendar from "./Calendar";
const App = () => {
return <Calendar />;
};
I have an App
component that renders a Calendar
component and an empty component.
import './calendar.css'
const Calendar = () => {
return 'calendar';
};
The only addition is the styles I prepared for our calendar. You can find the complete CSS in the source code at the end of this article.
Our first step now is to install Luxon in our project.
npm i luxon
Additionally, I want to install one more library called classnames
. It will allow us to concatenate classes in a convenient way.
npm i classnames
Preparing State
The two most important things for our calendar are to prepare an array of weekdays and to generate all the dates of the month that we want to render.
import { Info } from "luxon";
const Calendar = () => {
const today = DateTime.local();
const weekDays = Info.weekdays("short");
}
Here we leverage Luxon to create a today
DateTime and an array of weekdays in the short format.
This is how we can work with Luxon DateTime. As you can see, it is simple and efficient.
Now, we need to determine the first day of the month and store it in the state, as it will change every time we jump between months.
const Calendar = () => {
...
const [firstDayOfActiveMonth, setFirstDayOfActiveMonth] = useState(
today.startOf("month")
);
}
We store firstDayOfActiveMonth
in the state, and its initial value is based on today
, which we use to get the start of the month.
Now, we need to prepare an array of dates for the entire month that we want to render, including the end of the previous month or the beginning of the next month if they fall within the same weeks as the start and end of the current month.
With Luxon, we can accomplish this by using an interval between two dates.
const Calendar = () => {
...
const daysOfMonth = Interval.fromDateTimes(
firstDayOfActiveMonth.startOf("week"),
firstDayOfActiveMonth.endOf("month").endOf("week")
)
.splitBy({ day: 1 })
.map((day) => day.start);
}
We generate an interval between the start of the month and the end of the month, including the start and end of the week days. Then, we split the interval by 1 day and map a date for each day.
This is how our array looks. Each element is a Luxon DateTime object representing a specific day. As you can see, Luxon allows us to generate such an array in just 3 lines of code.
Rendering markup
Let's begin with some rendering.
return (
<div className="calendar-container">
<div className="calendar">
<div className="calendar-headline">
<div className="calendar-headline-month">
{firstDayOfActiveMonth.monthShort}, {firstDayOfActiveMonth.year}
</div>
<div className="calendar-headline-controls">
<div
className="calendar-headline-control"
>
«
</div>
<div
className="calendar-headline-control calendar-headline-controls-today"
>
Today
</div>
<div
className="calendar-headline-control"
>
»
</div>
</div>
</div>
</div>
</div>
);
Here is some markup for the headline of the calendar. We render the month name, year, and controls.
Now, let's render a list of weekdays and a grid for the month.
return (
<div className="calendar-container">
<div className="calendar">
<div className="calendar-headline">
...
</div>
<div className="calendar-weeks-grid">
{weekDays.map((weekDay, weekDayIndex) => (
<div key={weekDayIndex} className="calendar-weeks-grid-cell">
{weekDay}
</div>
))}
</div>
<div className="calendar-grid">
{daysOfMonth.map((dayOfMonth, dayOfMonthIndex) => (
<div
key={dayOfMonthIndex}
className="calendar-grid-cell"
>
{dayOfMonth.day}
</div>
))}
</div>
</div>
</div>
);
Here we mapped through weekDays
and daysOfMonth
to render the entire UI of the calendar.
As you can see, the entire calendar is correctly rendered.
Adding Meetings
The next feature we need to implement is rendering a list of meetings on the right side of our calendar. To achieve this, we first need to pass this meeting information to our calendar component.
const App = () => {
const meetings = {
"2024-04-05": ["Drink Coffee", "Learn React", "Sleep"],
"2024-04-06": ["Drink Coffee", "Learn Angular", "Sleep"],
};
return <Calendar meetings={meetings} />;
};
const Calendar = ({ meetings }) => {
...
}
We store meetings
as an object with a specific day as the key. This way, we can retrieve the list of meetings for any date.
Now, we need to store an active day in the state. This is necessary because when we click on a specific day to show its meetings, we need to store and update this information.
const Calendar = ({ meetings }) => {
const [activeDay, setActiveDay] = useState(null);
const activeDayMeetings = meetings[activeDay?.toISODate()] ?? [];
...
}
We will store the Luxon DateTime object in activeDay
, which will allow us to easily retrieve and display a list of meetings for the active day. With this setup, we have all the necessary information to render the list of meetings.
return (
<div className="calendar-container">
<div className="calendar">
...
</div>
<div className="schedule">
<div className="schedule-headline">
{activeDay === null && <div>Please select a day</div>}
{activeDay && (
<div>{activeDay.toLocaleString(DateTime.DATE_MED)}</div>
)}
</div>
<div>
{activeDay && activeDayMeetings.length === 0 && (
<div>No Planned Meetings Today</div>
)}
{activeDay && activeDayMeetings.length > 0 && (
<>
{activeDayMeetings.map((meeting, meetingIndex) => (
<div key={meetingIndex}>{meeting}</div>
))}
</>
)}
</div>
</div>
</div>
);
If there is no activeDay
, we render a label saying "Please select a day". Once a day is selected, we render at least a headline. If there are any meetings for this day, we render them as a list; otherwise, we render "No Planned Meetings Today".
The meetings list functionality is complete, but we still need to implement a click event to set an activeDay
.
{daysOfMonth.map((dayOfMonth, dayOfMonthIndex) => (
<div
...
onClick={() => setActiveDay(dayOfMonth)}
>
{dayOfMonth.day}
</div>
))}
We use setActiveDay
to update the state to the clicked day.
Now you can see that a day can be selected, and we render the correct information accordingly.
Highlighting dates
One more feature we're missing is highlighting the active day. To accomplish this, we can utilize the classnames
library that we've already installed.
import classnames from "classnames";
const Calendar = ({ meetings }) => {
...
{daysOfMonth.map((dayOfMonth, dayOfMonthIndex) => (
<div
key={dayOfMonthIndex}
className={classnames({
"calendar-grid-cell": true,
"calendar-grid-cell-inactive":
dayOfMonth.month !== firstDayOfActiveMonth.month,
"calendar-grid-cell-active":
activeDay?.toISODate() === dayOfMonth.toISODate(),
})}
onClick={() => setActiveDay(dayOfMonth)}
>
{dayOfMonth.day}
</div>
))}
}
Now, instead of applying a single class to our cell, we use the classnames
function to concatenate them dynamically. By default, we set calendar-grid-cell
for all cells. If the day is not in the current month, we add calendar-grid-cell-inactive
. When the day is active, we add calendar-grid-cell-active
.
As you can see, now the active day is correctly highlighted.
Navigation
The last feature we're missing is arrow navigation and jumping to today. To implement this, we need to create several functions for changing firstDayOfActiveMonth
. These functions will handle navigating to previous and next months, as well as jumping to today's date.
const Calendar = ({ meetings }) => {
...
const goToPreviousMonth = () => {
setFirstDayOfActiveMonth(firstDayOfActiveMonth.minus({ month: 1 }));
};
const goToNextMonth = () => {
setFirstDayOfActiveMonth(firstDayOfActiveMonth.plus({ month: 1 }));
};
const goToToday = () => {
setFirstDayOfActiveMonth(today.startOf("month"));
};
...
}
Here, we use Luxon to subtract or add a month from the firstDayOfActiveMonth
. Now, we just need to add click events to trigger these functions.
<div className="calendar-headline-controls">
<div
className="calendar-headline-control"
onClick={() => goToPreviousMonth()}
>
«
</div>
<div
className="calendar-headline-control calendar-headline-controls-today"
onClick={() => goToToday()}
>
Today
</div>
<div
className="calendar-headline-control"
onClick={() => goToNextMonth()}
>
»
</div>
</div>
We can navigate through the calendar by clicking on our headline controls.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done