Shipnative
ShipnativeShipnative
Sign in
GuideSeptember 2026 · 9 min read

React Native Calendar: Which Library, and the Date Bug

“Add a calendar” is three unrelated jobs wearing the same word. A month grid with dots on the days you did something. A scrolling agenda of upcoming items. A day timeline with draggable events at specific times. They need different libraries, and picking the wrong one means either a blank month you can’t put events on or a 200 KB timeline engine rendering thirty grey squares. Then, whichever you choose, there’s the bug every date-keyed app ships at least once: every date is one day off, but only for users west of London.

Pick the shape first

LibraryShapeNative codeUse it for
react-native-calendarsMonth grid, multi-month list, agendaNone — pure JSMarked days, streaks, date ranges, bookings
flash-calendarMonth grid on FlashListNone — pure JSLong scrollable ranges where the grid feels heavy
Calendar-kit style timelineDay / week timeline with eventsGesture handler + ReanimatedScheduling, shifts, room booking
Native date pickerSystem wheel or dialogNative modulePick one date — no calendar needed

The question that decides it: does anything in your app happen at a time, or only on a day? A habit tracker, a period tracker, a streak, a booking availability grid — those are day-level, and a month grid is the whole feature. A shift roster or a therapy schedule is time-level, and a month grid will never be enough no matter how much you extend it. Deciding this before you install saves you the migration.

The bug: your dates are one day off

Every calendar library keys on 'YYYY-MM-DD' strings. The obvious way to produce one is the wrong way:

// WRONG — toISOString() converts to UTC first.
// 8 Sept, 9pm in New York  ->  '2026-09-09'
const key = new Date().toISOString().slice(0, 10);

// RIGHT — read the local calendar fields.
export function toDayKey(d: Date): string {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

This is invisible in development if you build in Europe and your test data is generated at midday. It shows up as one-star reviews from users in the Americas saying the app marks yesterday, or that their streak resets at 7pm. The same class of bug bites the other way for users ahead of UTC.

The matching trap is stepping through a range by adding 86400000 milliseconds. On the two days a year the clocks change, that lands you on 23:00 the previous day or 01:00 the next, and your range silently skips or repeats a square. Step with setDate(d.getDate() + 1), which is calendar-aware:

export function daysBetween(start: Date, end: Date): string[] {
  const out: string[] = [];
  const cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
  const last = new Date(end.getFullYear(), end.getMonth(), end.getDate());
  while (cursor <= last) {
    out.push(toDayKey(cursor));
    cursor.setDate(cursor.getDate() + 1);   // DST-safe
  }
  return out;
}

A month grid that marks your data

The common screen — a month, dots on days that have entries, tap a day to filter the list below — is about forty lines. The part that matters is that markedDates is derived, memoized, and merged with the selection rather than rebuilt inline:

import { useMemo, useState } from 'react';
import { View } from 'react-native';
import { Calendar, LocaleConfig } from 'react-native-calendars';

LocaleConfig.defaultLocale = 'en';   // set once, at module scope

type Entry = { id: string; at: Date };

export function EntryCalendar({ entries }: { entries: Entry[] }) {
  const [selected, setSelected] = useState(() => toDayKey(new Date()));

  // Derived, not rebuilt in JSX — markedDates is compared by reference.
  const marked = useMemo(() => {
    const map: Record<string, any> = {};
    for (const e of entries) {
      const key = toDayKey(e.at);
      map[key] = { marked: true, dotColor: '#fb923c' };
    }
    map[selected] = { ...(map[selected] ?? {}), selected: true, selectedColor: '#fb923c' };
    return map;
  }, [entries, selected]);

  return (
    <View>
      <Calendar
        markedDates={marked}
        onDayPress={(day) => setSelected(day.dateString)}   // already local YYYY-MM-DD
        firstDay={1}                                        // Monday — match the region
        enableSwipeMonths
        theme={{
          calendarBackground: '#1c1c1c',
          dayTextColor: 'rgba(255,255,255,0.85)',
          monthTextColor: '#ffffff',
          textDisabledColor: 'rgba(255,255,255,0.25)',
          todayTextColor: '#fb923c',
          arrowColor: '#fb923c',
        }}
      />
    </View>
  );
}

Two details worth keeping. onDayPress hands you a dateString that is already a local day key — use it directly instead of converting it back through a Date, which is where the off-by-one usually creeps back in. And firstDay defaults to Sunday; most of Europe expects Monday, and getting it wrong makes the whole grid feel foreign in a way users notice but rarely report.

Marking a range

Booking flows and period trackers need a filled band rather than dots. The library draws it, but you have to enumerate the days yourself — passing two endpoints does nothing:

const rangeMarks = useMemo(() => {
  if (!from) return {};
  const days = daysBetween(from, to ?? from);
  const marks: Record<string, any> = {};
  days.forEach((key, i) => {
    marks[key] = {
      color: '#fb923c',
      textColor: '#1c1c1c',
      startingDay: i === 0,
      endingDay: i === days.length - 1,
    };
  });
  return marks;
}, [from, to]);

<Calendar markingType="period" markedDates={rangeMarks} onDayPress={handleRangePress} />

For a booking calendar, add the disabled days in the same pass — disabled: true plus disableTouchEvent: trueon dates that are already taken. Doing it in one memo keeps availability and selection from disagreeing, which is the usual source of “it let me book a day that was full”.

The traps

  1. Inline markedDates. Building the object in JSX makes a new reference on every render, so every day cell re-renders whenever any state on the screen changes. On a screen with a text input above the calendar, that is every keystroke.
  2. toISOString for day keys.Covered above, and worth a lint rule. If a date only ever means “a day”, store the string, not a timestamp.
  3. An unmemoized custom dayComponent. A custom cell defined inside the parent is a new component type every render, so React unmounts and remounts all 35 cells. Define it at module scope and wrap it in React.memo.
  4. Reaching for the agenda view too early. The agenda component is the heaviest thing in the package. A month grid plus a plain FlatList of the selected day’s items is lighter, easier to style, and what most apps actually want.
  5. Installing a calendar to pick one date.If the interaction is “when is your birthday”, the native date picker is one component, zero styling, and already familiar to the user.

One more, for anything with reminders: a day key is not a moment. When you turn '2026-09-08' back into a notification time, construct it in local time with an explicit hour rather than parsing the bare string, which most engines read as midnight UTC.

The shortcut: describe the screen, not the marking

Nothing here is hard, but all of it is fiddly, and the date handling is the kind of thing that looks correct on your machine and is wrong for a third of your users. The wiring is also identical in every app that has ever put dots on a month.

Describe the screen in ShipNative — “a month calendar where days with a logged workout show a dot, tapping a day lists that day’s workouts underneath” — and it installs the calendar, derives the marked-days map from your data model, and runs it on your phone so you can check the marking and the month swipe on real hardware. Export the full Expo project whenever you want it.

Build it free

Describe your app in one sentence and have it running on your own phone in minutes at shipnative.dev. No credit card, full React Native source export whenever you want it.

Frequently Asked Questions

What is the best calendar library for React Native?

For a month grid with marked days — the shape most apps need — react-native-calendars is still the default: it is mature, works on both platforms, and handles marking, ranges, and locales. If you are rendering long scrollable ranges and hitting jank, flash-calendar is built on FlashList and is noticeably lighter. If you need a day or week timeline with draggable events, neither fits and you want a calendar-kit style library instead. If you only need the user to pick one date, do not install a calendar at all — use the native date picker.

Why is my React Native calendar showing the wrong day?

Almost always because a Date was converted with toISOString(), which converts to UTC first. In any timezone behind UTC, the local evening of the 8th becomes the 9th in UTC, so the marked day lands one square off. Calendar libraries key everything on YYYY-MM-DD strings, so build those strings from the local getFullYear/getMonth/getDate values, never from toISOString().slice(0, 10).

Why does my calendar re-render slowly or flicker on every tap?

The markedDates prop is compared by reference. Building the object inline in JSX creates a new one every render, so the calendar re-renders all of its day cells on every unrelated state change. Wrap it in useMemo keyed on the underlying data and the selected date, and memoize any custom dayComponent.

Can I mark a date range in react-native-calendars?

Yes — set markingType to "period" and give the first day startingDay: true, the last endingDay: true, and every day in between just color and textColor. You have to generate that intermediate list yourself; the library does not fill a range from two endpoints. Iterate day by day in local time rather than adding 86400000 milliseconds, so daylight-saving transitions do not skip or repeat a day.

Can an AI app builder wire the calendar for me?

Yes. Describe the screen — "a month calendar where days with a logged workout show a dot, and tapping a day shows that day's entries below" — and ShipNative installs the calendar, builds the marked-days map from your data, and previews it on your phone so you can check the marking and the month swipe on real hardware.

→

React Native Date Picker

If the user only needs to choose one date, this is the cheaper answer.

Read guide →
→

React Native FlatList

The list underneath an agenda view — and why it janks.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.