Kalender-App in React Native
Kalender-App
~12 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
Ein selbst gebauter Monatskalender bündelt viele Themen dieses Tutorials: Datumsberechnung in reinem JavaScript, ein Raster-Layout mit FlatList/numColumns, Auswahl-State und eine Ereignisliste pro Tag.
1. Beschreibung
React Native hat KEINE eingebaute Kalender-Komponente. Ein Monatsraster wird selbst berechnet: der erste Wochentag des Monats bestimmt, wie viele leere Zellen am Anfang stehen, new Date(jahr, monat + 1, 0).getDate() liefert die Anzahl der Tage im Monat.
2. Kurzes Beispiel
const tageImMonat = new Date(jahr, monat + 1, 0).getDate();
const ersterWochentag = new Date(jahr, monat, 1).getDay(); // 0 = Sonntag3. Komplettes Projekt: Monatskalender mit Terminen
npx create-expo-app calendar-app
cd calendar-appconst WOCHENTAGE = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
const MONATSNAMEN = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember',
];
// Erzeugt ein Raster aus Tageszahlen (null = leere Füllzelle vor dem 1.)
export function erzeugeMonatsraster(jahr, monat) {
const tageImMonat = new Date(jahr, monat + 1, 0).getDate();
const ersterWochentag = new Date(jahr, monat, 1).getDay();
const zellen = [];
for (let i = 0; i < ersterWochentag; i++) {
zellen.push(null);
}
for (let tag = 1; tag <= tageImMonat; tag++) {
zellen.push(tag);
}
return zellen;
}
export function schluesselFuer(jahr, monat, tag) {
return `${jahr}-${String(monat + 1).padStart(2, '0')}-${String(tag).padStart(2, '0')}`;
}
export { WOCHENTAGE, MONATSNAMEN };import { useState, useMemo } from 'react';
import { View, Text, TouchableOpacity, FlatList, StyleSheet } from 'react-native';
import { erzeugeMonatsraster, schluesselFuer, WOCHENTAGE, MONATSNAMEN } from './utils/kalender';
const TERMINE = {
[schluesselFuer(new Date().getFullYear(), new Date().getMonth(), 5)]: ['Zahnarzt 10:00'],
[schluesselFuer(new Date().getFullYear(), new Date().getMonth(), 14)]: ['Teammeeting 14:00', 'Geburtstag Anna'],
};
export default function App() {
const heute = new Date();
const [jahr, setJahr] = useState(heute.getFullYear());
const [monat, setMonat] = useState(heute.getMonth());
const [ausgewaehlterTag, setAusgewaehlterTag] = useState(heute.getDate());
const zellen = useMemo(() => erzeugeMonatsraster(jahr, monat), [jahr, monat]);
function monatWechseln(differenz) {
const neuesDatum = new Date(jahr, monat + differenz, 1);
setJahr(neuesDatum.getFullYear());
setMonat(neuesDatum.getMonth());
setAusgewaehlterTag(null);
}
const ausgewaehlterSchluessel = ausgewaehlterTag
? schluesselFuer(jahr, monat, ausgewaehlterTag)
: null;
const termineAmTag = TERMINE[ausgewaehlterSchluessel] ?? [];
return (
<View style={styles.container}>
<View style={styles.kopfzeile}>
<TouchableOpacity onPress={() => monatWechseln(-1)}>
<Text style={styles.pfeil}>‹</Text>
</TouchableOpacity>
<Text style={styles.monatsTitel}>{MONATSNAMEN[monat]} {jahr}</Text>
<TouchableOpacity onPress={() => monatWechseln(1)}>
<Text style={styles.pfeil}>›</Text>
</TouchableOpacity>
</View>
<View style={styles.wochentagZeile}>
{WOCHENTAGE.map((w) => (
<Text key={w} style={styles.wochentag}>{w}</Text>
))}
</View>
<FlatList
data={zellen}
numColumns={7}
keyExtractor={(_, index) => String(index)}
renderItem={({ item: tag }) => {
if (tag === null) return <View style={styles.zelle} />;
const hatTermin = Boolean(TERMINE[schluesselFuer(jahr, monat, tag)]);
const istAusgewaehlt = tag === ausgewaehlterTag;
return (
<TouchableOpacity
style={styles.zelle}
onPress={() => setAusgewaehlterTag(tag)}
>
<View style={[styles.tagKreis, istAusgewaehlt && styles.tagKreisAktiv]}>
<Text style={[styles.tagText, istAusgewaehlt && styles.tagTextAktiv]}>
{tag}
</Text>
</View>
{hatTermin && <View style={styles.terminPunkt} />}
</TouchableOpacity>
);
}}
/>
<View style={styles.termineBereich}>
<Text style={styles.termineTitel}>
Termine {ausgewaehlterTag ? `am ${ausgewaehlterTag}.` : ''}
</Text>
{termineAmTag.length === 0 && (
<Text style={styles.keineTermine}>Keine Termine.</Text>
)}
{termineAmTag.map((termin) => (
<Text key={termin} style={styles.termin}>• {termin}</Text>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 60, paddingHorizontal: 16 },
kopfzeile: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
pfeil: { fontSize: 24, paddingHorizontal: 16, color: '#2563eb' },
monatsTitel: { fontSize: 18, fontWeight: 'bold' },
wochentagZeile: { flexDirection: 'row', marginBottom: 4 },
wochentag: { flex: 1, textAlign: 'center', color: '#9ca3af', fontSize: 12 },
zelle: { flex: 1, aspectRatio: 1, alignItems: 'center', justifyContent: 'center' },
tagKreis: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
tagKreisAktiv: { backgroundColor: '#2563eb' },
tagText: { fontSize: 14 },
tagTextAktiv: { color: 'white', fontWeight: 'bold' },
terminPunkt: { width: 5, height: 5, borderRadius: 2.5, backgroundColor: '#f59e0b', marginTop: 2 },
termineBereich: { marginTop: 20, borderTopWidth: 1, borderTopColor: '#e5e7eb', paddingTop: 12 },
termineTitel: { fontWeight: 'bold', marginBottom: 8 },
keineTermine: { color: '#9ca3af' },
termin: { marginBottom: 4 },
});4. Erklärung
erzeugeMonatsraster()ist REINE Berechnungslogik ohne React-Bezug – sie könnte genauso in einem Node.js-Skript oder Unit-Test verwendet werden, ein bewusst sauber getrennter Baustein.useMemo(() => ..., [jahr, monat])verhindert, dass das Raster bei JEDEM Render neu berechnet wird (z.B. beim Auswählen eines Tages), sondern nur, wenn sich Jahr oder Monat tatsächlich ändern.numColumns={{7}}aufFlatListerzeugt das Wochenraster –null-Einträge am Monatsanfang werden als leere, aber gleich großeViewgerendert, damit das Raster optisch nicht verrutscht.TERMINEist ein Objekt, dessen Schlüssel'JJJJ-MM-TT'-Strings sind (schluesselFuer()) – ein gängiges Muster, um Datum-basierte Nachschlagen ohne teure Array-Suche in O(1) zu erledigen.