Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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 = Sonntag

3. Komplettes Projekt: Monatskalender mit Terminen

npx create-expo-app calendar-app
cd calendar-app
utils/kalender.js
const 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 };
App.js
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}} auf FlatList erzeugt das Wochenraster – null-Einträge am Monatsanfang werden als leere, aber gleich große View gerendert, damit das Raster optisch nicht verrutscht.
  • TERMINE ist 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.

5. Ausgaben

Ausgabe
Kopfzeile mit "‹ [aktueller Monat] [Jahr] ›", darunter Wochentags-Kürzel (So–Sa), darunter ein 7-spaltiges Zahlenraster. Tage mit Terminen zeigen einen kleinen orangen Punkt darunter; der ausgewählte Tag ist blau hervorgehoben. Darunter eine "Termine am [Tag]."-Liste, z.B. "• Teammeeting 14:00" und "• Geburtstag Anna" für den 14.