Flash Calendar Documentation

repository·main·Indexed 23 days ago

https://github.com/marceloprado/flash-calendar

A high-performance, flexible calendar library for React Native and Expo. Built on Shopify's FlashList, it is optimized for infinite lists, date pickers, and range pickers with a small bundle size (~18.8kb). It features a hierarchical component anatomy (Calendar, Row, Item), a flexible theme prop for styling, and a Date ID system (YYYY-MM-DD) to prevent timezone bugs. The library allows developers to bring their own date formatting library and provides imperative scroll control via CalendarListRef.

Tokens
15.2K
Snippets
24
Records
68
Agent score
81%

What's inside Flash Calendar

  1. Overview of Flash Calendar features

    main

    Flash Calendar is a high-performance calendar library for React Native designed for speed and flexibility. Key features include:

    • Platform Support: Works on both iOS and Android.
    • Expo Compatibility: Works with Expo without requiring custom native binary updates.
    • Performance: Uses infinite scroll and has a tiny bundle size (~18kb minified, 6kb gzip) with only one small third-party dependency (mitt).
    • UI/UX: Built-in localization, dark-mode support, and customizable/composable UI components.
    • Functionality: Supports date range selection and is compatible with react-native-bottom-sheet.
  2. Localization and custom date formatting

    main

    Flash Calendar follows a "bring your own date library" approach.

    Simple Localization

    Use calendarFormatLocale to pass a locale string (e.g., 'pt-BR').

    Full Customization

    Use the following props to provide custom formatting functions (compatible with libraries like date-fns):

    • getCalendarDayFormat
    • getCalendarMonthFormat
    • getCalendarWeekDayFormat

    CRITICAL: Referential Equality To prevent performance issues and unnecessary re-renders, formatting functions must be stable. Do not define them inline inside the component. Instead, define them outside the component scope or wrap them in useCallback.

    import { Calendar, toDateId } from "@marceloterreiro/flash-calendar";
    import { subMonths } from "date-fns";
    import { format } from "date-fns/fp";
    
    const threeMonthsAgo = subMonths(new Date(), 3);
    
    export const CalendarCustomFormatting = () => {
      return (
        <Calendar
          calendarMonthId={toDateId(threeMonthsAgo)}
          getCalendarDayFormat={format("dd")}
          getCalendarMonthFormat={format("MMMM yyyy (LL/yyyy)")}
          getCalendarWeekDayFormat={format("E")}
          onCalendarDayPress={(dateId) => {
            console.log(`Clicked on ${dateId}`);
          }}
        />
      );
    };
  3. Bring your own date formatting library

    main
    To maintain a tiny footprint and avoid unnecessary bloat, Flash Calendar does not include a built-in date formatting library. Instead, it allows you to bring your own date formatting library (such as moment, date-fns, luxon, or dayjs). This gives you full control over how dates are formatted and localized within your application without adding extra dependencies to your bundle.
  4. Understand the Flash Calendar component anatomy

    main

    The Flash Calendar is composed of several hierarchical components. Understanding this structure is essential for both styling via the theme prop and for composing custom calendars.

    ComponentDescriptionLayout-related props
    CalendarThe main calendar component.calendarSpacing
    Calendar.Row.MonthRenders the month row.calendarMonthHeaderHeight, getCalendarMonthFormat
    Calendar.Row.WeekRenders each week row, including week day names.calendarWeekHeaderHeight, calendarRowHorizontalSpacing, calendarRowVerticalSpacing
    Calendar.Item.DayRenders the day item (e.g. 1, 2, 3).calendarDayHeight, getCalendarDayFormat
    Calendar.Item.WeekNameRenders the week day name (e.g. Sun, Mon).getCalendarWeekDayFormat, calendarFirstDayOfWeek
    Calendar.Item.EmptyRenders an empty item to fill the grid at the start/end of a month.
  5. Performance characteristics of Flash Calendar

    main

    Flash Calendar is optimized for high-performance use cases including infinite lists, date pickers, and date range pickers. It achieves this through two primary mechanisms:

    1. Minimized Re-renders: The library is designed so that only the affected dates re-render during updates, ensuring the component remains responsive even with lengthy calendar lists.
    2. Scroll Performance: The <Calendar.List /> component is a wrapper around Shopify's FlashList. Consequently, it inherits all of FlashList's performance characteristics and optimization benefits. Developers should follow FlashList's performance tuning guides when using Flash Calendar lists.
  6. Workaround for infinite backward scrolling limitation

    main

    Currently, Flash Calendar's infinite scrolling does not work backwards. While onEndReached allows appending new months as the user scrolls forward, there is no onStartReached implementation to prepend months while maintaining scroll position. Consequently, the calendar will not scroll past the initial month when scrolling upwards.

    To enable backward scrolling, you can preload the required range of past months using calendarMinDateId and calendarPastScrollRangeInMonths.

    Note on Performance: Preloading a large number of months increases memory usage because the month list is held in memory. However, due to the efficiency of Flash List, rendering a large number of months (e.g., 2,000 months/166 years) is generally performant.

    export const ScrollingBackwardsWorkaround = () => {
      return (
        <VStack alignItems="stretch" grow spacing={12}>
          <Text>This preloads all past months between Jan 1st 2020 and today</Text>
    
          <Calendar.List
            calendarFutureScrollRangeInMonths={1}
            calendarInitialMonthId="2024-02-01"
            calendarMaxDateId="2024-05-01"
            calendarMinDateId="2020-01-01"
            calendarPastScrollRangeInMonths={50}
            onCalendarDayPress={loggingHandler("onCalendarDayPress")}
          />
        </VStack>
      );
    };
  7. Deploy the documentation website

    main

    You can deploy the documentation website using either SSH or GitHub username configuration. If you are hosting on GitHub Pages, providing your GitHub username will allow the command to build the site and push it to the gh-pages branch.

    Using SSH: Set USE_SSH=true before running the deploy command.

    Using GitHub (Non-SSH): Set GIT_USER to your GitHub username before running the deploy command.

  8. Install @marceloterreiro/flash-calendar

    main

    To use Flash Calendar in your React Native project, install the package via npm or yarn.

    Note: You must also install @shopify/flash-list as a peer dependency. Please refer to the @shopify/flash-list documentation for specific installation instructions and requirements.

    npm add @marceloterreiro/flash-calendar
  9. Optimize `Calendar.List` performance when using date ranges

    main

    When using calendarActiveDateRanges in <Calendar.List />, improper state management can cause the entire list to re-render whenever the ranges change, leading to frame drops.

    1. Use useDateRange: The easiest and most optimized way to handle date ranges is to use the built-in useDateRange hook.
    2. Manual Implementation: If you must manually control onCalendarDayPress, you must ensure performance by:
      • Memoizing the onCalendarDayPress function using useCallback.
      • Using the updater function pattern (e.g., setDates(prev => ... )) when updating state. This keeps the props of the underlying BaseCalendar components stable, preventing unnecessary re-renders of every month in the list.
    import type { CalendarOnDayPress } from "@marceloterreiro/flash-calendar";
    import { Calendar, toDateId } from "@marceloterreiro/flash-calendar";
    import { addMonths } from "date-fns";
    import { useCallback, useState } from "react";
    import { Text } from "react-native";
    
    const todayId = toDateId(new Date());
    const maxDateId = toDateId(addMonths(new Date(), 12));
    
    export const SlowExampleAddressed = () => {
      const [dateIds, setDateIds] = useState<string[]>([]);
      const dateRanges = dateIds.map((dateId) => ({
        startId: dateId,
        endId: dateId,
      }));
    
      // This is the fix: memoized onCalendarDayPress and updater function pattern
      // It keeps `BaseCalendar` props stable, allowing each month to skip re-renders
      const handleCalendarDayPress = useCallback<CalendarOnDayPress>((dateId) => {
        setDateIds((dateIds) => {
          if (dateIds.includes(dateId)) {
            return dateIds.filter((id) => id !== dateId);
          } else {
            return [...dateIds, dateId];
          }
        });
      }, []);
    
      return (
        <Calendar.VStack alignItems="stretch" grow spacing={12}>
          <Text>✅ This is safe to copy, perf issues addressed</Text>
    
          <Calendar.List
            calendarActiveDateRanges={dateRanges}
            calendarInitialMonthId={todayId}
            calendarMaxDateId={maxDateId}
            calendarMinDateId={todayId}
            onCalendarDayPress={handleCalendarDayPress}
          />
        </Calendar.VStack>
      );
    };
  10. Use Calendar.List with a Bottom Sheet

    main

    To use Calendar.List inside a component like react-native-bottom-sheet (especially on Android), you can replace the default scroll component by passing a custom component to the CalendarScrollComponent prop.

    import BottomSheet from "@gorhom/bottom-sheet";
    import { Calendar } from "@marceloterreiro/flash-calendar";
    import { FlashList } from "@shopify/flash-list";
    import React, { useMemo, useRef } from "react";
    import { Platform, StyleSheet, View } from "react-native";
    
    // Note: Implementation of BottomSheetFlashList is required for Android
    const SafeFlashList = Platform.select({
      android: BottomSheetFlashList,
      ios: FlashList,
    });
    
    export const BottomSheetCalendar = () => {
      const bottomSheetRef = useRef<BottomSheet>(null);
      const snapPoints = useMemo(() => ["25%", "50%"], []);
    
      return (
        <View style={styles.container}>
          <BottomSheet index={1} ref={bottomSheetRef} snapPoints={snapPoints}>
            <View style={styles.contentContainer}>
              <Calendar.List
                CalendarScrollComponent={SafeFlashList}
                calendarInitialMonthId="2024-02-01"
                onCalendarDayPress={(dateId) => console.log(`Pressed ${dateId}`)}
              />
            </View>
          </BottomSheet>
        </View>
      );
    };
  11. Support hover and focus states in the theme

    main

    To support web interactions (like Pressable interaction states in react-native-web), you can use isHovered and isFocused within your CalendarTheme style functions for itemDay states (idle and today).

    const linearTheme: CalendarTheme = {
      // ... other theme properties
      itemDay: {
        idle: ({ isPressed, isHovered, isWeekend }) => ({
          container: {
            backgroundColor: isPressed || isHovered ? linearAccent : "transparent",
            borderRadius: 4,
          },
          content: {
            color: isWeekend && !isPressed ? "rgba(255, 255, 255, 0.5)" : "#ffffff",
          },
        }),
        today: ({ isPressed, isHovered }) => ({
          container: {
            borderColor: "rgba(255, 255, 255, 0.5)",
            borderRadius: isPressed || isHovered ? 4 : 30,
            backgroundColor: isPressed || isHovered ? linearAccent : "transparent",
          },
          content: {
            color: isPressed || isHovered ? "#ffffff" : "rgba(255, 255, 255, 0.5)",
          },
        }),
        // ...
      },
    };