Calendar App in React Native
Calendar App
~12 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
A hand-built month calendar bundles many topics from this tutorial: date arithmetic in plain JavaScript, a grid layout with FlatList/numColumns, selection state, and a per-day event list.
1. Description
React Native has NO built-in calendar component. A month grid is computed yourself: the month's first weekday determines how many empty cells sit at the start, new Date(year, month + 1, 0).getDate() gives the number of days in the month.
2. Short example
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstWeekday = new Date(year, month, 1).getDay(); // 0 = Sunday3. Complete project: a month calendar with events
npx create-expo-app calendar-app
cd calendar-appconst WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
// Produces a grid of day numbers (null = empty filler cell before the 1st)
export function buildMonthGrid(year, month) {
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstWeekday = new Date(year, month, 1).getDay();
const cells = [];
for (let i = 0; i < firstWeekday; i++) {
cells.push(null);
}
for (let day = 1; day <= daysInMonth; day++) {
cells.push(day);
}
return cells;
}
export function keyFor(year, month, day) {
return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
export { WEEKDAYS, MONTH_NAMES };import { useState, useMemo } from 'react';
import { View, Text, TouchableOpacity, FlatList, StyleSheet } from 'react-native';
import { buildMonthGrid, keyFor, WEEKDAYS, MONTH_NAMES } from './utils/calendar';
const EVENTS = {
[keyFor(new Date().getFullYear(), new Date().getMonth(), 5)]: ['Dentist 10:00'],
[keyFor(new Date().getFullYear(), new Date().getMonth(), 14)]: ['Team meeting 14:00', "Anna's birthday"],
};
export default function App() {
const today = new Date();
const [year, setYear] = useState(today.getFullYear());
const [month, setMonth] = useState(today.getMonth());
const [selectedDay, setSelectedDay] = useState(today.getDate());
const cells = useMemo(() => buildMonthGrid(year, month), [year, month]);
function changeMonth(delta) {
const newDate = new Date(year, month + delta, 1);
setYear(newDate.getFullYear());
setMonth(newDate.getMonth());
setSelectedDay(null);
}
const selectedKey = selectedDay
? keyFor(year, month, selectedDay)
: null;
const eventsOnDay = EVENTS[selectedKey] ?? [];
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => changeMonth(-1)}>
<Text style={styles.arrow}>‹</Text>
</TouchableOpacity>
<Text style={styles.monthTitle}>{MONTH_NAMES[month]} {year}</Text>
<TouchableOpacity onPress={() => changeMonth(1)}>
<Text style={styles.arrow}>›</Text>
</TouchableOpacity>
</View>
<View style={styles.weekdayRow}>
{WEEKDAYS.map((w) => (
<Text key={w} style={styles.weekday}>{w}</Text>
))}
</View>
<FlatList
data={cells}
numColumns={7}
keyExtractor={(_, index) => String(index)}
renderItem={({ item: day }) => {
if (day === null) return <View style={styles.cell} />;
const hasEvent = Boolean(EVENTS[keyFor(year, month, day)]);
const isSelected = day === selectedDay;
return (
<TouchableOpacity
style={styles.cell}
onPress={() => setSelectedDay(day)}
>
<View style={[styles.dayCircle, isSelected && styles.dayCircleActive]}>
<Text style={[styles.dayText, isSelected && styles.dayTextActive]}>
{day}
</Text>
</View>
{hasEvent && <View style={styles.eventDot} />}
</TouchableOpacity>
);
}}
/>
<View style={styles.eventsSection}>
<Text style={styles.eventsTitle}>
Events {selectedDay ? `on the ${selectedDay}.` : ''}
</Text>
{eventsOnDay.length === 0 && (
<Text style={styles.noEvents}>No events.</Text>
)}
{eventsOnDay.map((event) => (
<Text key={event} style={styles.event}>• {event}</Text>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 60, paddingHorizontal: 16 },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
arrow: { fontSize: 24, paddingHorizontal: 16, color: '#2563eb' },
monthTitle: { fontSize: 18, fontWeight: 'bold' },
weekdayRow: { flexDirection: 'row', marginBottom: 4 },
weekday: { flex: 1, textAlign: 'center', color: '#9ca3af', fontSize: 12 },
cell: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },
dayCircle: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
dayCircleActive: { backgroundColor: '#2563eb' },
dayText: { fontSize: 14 },
dayTextActive: { color: 'white', fontWeight: 'bold' },
eventDot: { width: 5, height: 5, borderRadius: 2.5, backgroundColor: '#f59e0b', marginTop: 2 },
eventsSection: { marginTop: 20, borderTopWidth: 1, borderTopColor: '#e5e7eb', paddingTop: 12 },
eventsTitle: { fontWeight: 'bold', marginBottom: 8 },
noEvents: { color: '#9ca3af' },
event: { marginBottom: 4 },
});4. Explanation
buildMonthGrid()is PURE calculation logic with no React dependency – it could be used just the same in a Node.js script or a unit test, a deliberately clean, separated building block.useMemo(() => ..., [year, month])prevents the grid from being recomputed on EVERY render (e.g. when selecting a day), only when the year or month actually changes.numColumns={{7}}onFlatListproduces the weekly grid –nullentries at the start of the month render as empty, but equally sized,Views, so the grid doesn't visually shift.EVENTSis an object whose keys are'YYYY-MM-DD'strings (keyFor()) – a common pattern for O(1) date-based lookups without an expensive array search.