Building a Custom Calendar Component in React Native: A Developer’s Journey

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:

  • Was easy to use and maintain (since it would be maintained in-house)
  • Was easy to style and customize with colors
  • Was fast and efficient
  • Automatically adjusted to the color scheme set by Tamagui
  • Used Tamagui components/layouts so it was optimized by the compiler and consistent with our UI
  • Didn’t use any external packages
  • Looked aesthetically pleasing


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.

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:

  • Single Date Picker: Focuses on a single date selection. The component only needed to manage and render a single chosen date.
  • Range Slider: Needed to handle start and end dates and highlight the range between them. This required additional logic to manage ranges appropriately.

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! 👩💻

To view or add a comment, sign in

Others also viewed

Explore content categories