Building a Custom Calendar Component in React Native: A Developer’s Journey
Hey there, fellow developers! 🚀
I’m excited to share my journey of building and optimising a custom calendar component in React Native.
This project came about because of a need for a calendar that seamlessly fit into the styling of the UI library I was using, tamagui. When I set out to find a library, I was optimistic. However, after trying libraries like react-native-calendars and react-native-big-calendar, I found that none of them even came close to being similar in design to our other components. Rather than try to recreate someone else's wheel, it became clear that I’d need to create my calendar component.
Challenge accepted! 💪
The Initial Motivation
First, a quick backstory for you. I was working on an application where I needed to integrate a calendar. I wanted a component that:
The Development Journey
The Basic Structure
I started by defining the basic structure of the calendar. The core idea was to represent each day as a clickable button that can change its styling based on a few conditions - whether it’s today, selected, part of the current month, etc.
interface CalendarDayProps {
date: Date;
selectedDate: Date | null;
currentMonth: number;
onDateSelect: (date: Date) => void;
// Additional optional styling props
}
const CalendarDay: React.FC<CalendarDayProps> = ({ date, selectedDate, currentMonth, onDateSelect, todayColor = '$blue10', selectedColor = '$background', defaultColor = '$color', currentMonthColor = '$color', otherMonthColor = '$gray7' }) => {
const isToday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
// ...
}
Here, we introduce CalendarDayProps to define the properties that our CalendarDay component will accept. The optional styling props allow customization as needed, helping keep the design flexible and, most importantly, reusable.
Calculating the Days
Next, I needed to generate the days for the current month, filling in the previous month’s days as necessary to complete the weeks.
const getDaysInMonth = (month: number, year: number) => {
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const days: Date[] = [];
for (let i = firstDay - 1; i >= 0; i--) {
days.unshift(new Date(year, month, -i));
}
for (let i = 1; i <= daysInMonth; i++) {
days.push(new Date(year, month, i));
}
return days;
};
This function, getDaysInMonth, correctly calculates the days to display in the calendar view. I filled in the starting days to align the calendar grid, ensuring each row had seven days.
Interactive Components
The SingleDatePicker was built to allow single-date selection. I used reacts useState to manage the current month/year and the selected date.
const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ onDateSelect, ...props }) => {
const [{ month, year }, setCalendarDate] = useState({
month: today.getMonth(),
year: today.getFullYear(),
});
const handleMonthChange = (offset: number) => {
// Update month and year accordingly
};
// ...
return (
<Stack {...props}>
<Card borderRadius="$4" padding="$4">
// ...Calendar header...
<XStack>
{weekDays.map((day) => (
<Paragraph key={day} textAlign="center" flexBasis="14.28%">
{day}
</Paragraph>
))}
</XStack>
// ...Days rendering...
</Card>
</Stack>
);
};
The above snippet shows some of the structure of the SingleDatePicker component, including the header navigation for changing months and rendering days.
In case you're curious, the 14.28% came from dividing the 100% width by 7 for the days of the week.
Introducing Range Selection
After successfully creating a single date picker, I needed a range selection feature. This allowed users to select a start and end date, highlighting the range between them.
Recommended by LinkedIn
Additional Logic for Range
In the range selection version, additional logic checks if a date lies within the selected range and appropriately styles those dates.
const isDateInRange = (date: Date, startDate: Date | null, endDate: Date | null) => {
return startDate && endDate && date >= startDate && date <= endDate;
};
This utility function checks if a date falls between the start and end date, used to highlight the range correctly.
Optimization Journey
After getting the basic functionality, it was time to optimize. My goals were performance improvements and reducing unnecessary re-renders.
Memoization and Callbacks
We leveraged useMemo and useCallback to wrap calculations and handlers, ensuring they are only recalculated when their dependencies change.
const handleMonthChange = useCallback((offset: number) => {
setCalendarDate((prev) => {
const newMonth = prev.month + offset;
const newYear = prev.year + Math.floor(newMonth / 12);
return { month: (newMonth + 12) % 12, year: newYear };
});
}, []);
Using useCallback, the handleMonthChange was optimised to avoid being recreated on every render, thus improving performance.
Using React.memo
For components like CalendarDay, wrapping them with React.memo helped prevent unnecessary re-renders.
const CalendarDay: React.FC<CalendarDayProps> = React.memo(({
// props definition
todayColor = "$color10", selectedColor = "white", currentMonthColor = "$color8", otherMonthColor = "$color6"
}) => {
// component logic
});
With React.memo, CalendarDay maintains performance by only re-rendering when its props actually change.
Compare and Contrast
Single Date Picker vs. Range Slider:
Both types benefited from similar optimizations, but the range slider had unique requirements for managing and displaying ranges, adding a layer of complexity.
Conclusion
Creating these calendar components was a fun challenge! 🎉 From initial conception to optimisations, every step brought new learning experiences. The use of hooks (useState, useMemo, useCallback) and React’s memoization (React.memo) were crucial in making efficient, flexible, and highly customizable components.
So, fellow devs, if you ever find existing solutions aren’t quite cutting it, don’t be afraid to throw yourselves into the deep end and build something custom. You might find the results are more satisfying and better suited to your app’s needs.
Happy coding! 👩💻