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

ToDo-App in React Native

ToDo-App

~12 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026

Die ToDo-App ist die klassische "Hello World" komplexerer mobiler Apps – sie bündelt fast alles aus diesem Tutorial in einem realistischen Projekt: Liste, Eingabe, Zustand, Persistenz, bedingtes Rendern und Löschen.

1. Beschreibung

Diese App verwaltet eine Liste von Aufgaben (Objekte mit id, text, erledigt), speichert sie dauerhaft mit AsyncStorage (siehe das gleichnamige Kapitel) und bietet Hinzufügen, Erledigt-Markieren, Löschen sowie einen Filter (Alle/Offen/Erledigt).

2. Kurzes Beispiel

const [aufgaben, setAufgaben] = useState([]);

function hinzufuegen(text) {
  setAufgaben((liste) => [...liste, { id: Date.now(), text, erledigt: false }]);
}

3. Komplettes Projekt: Persistente ToDo-App

npx create-expo-app todo-app
cd todo-app
npx expo install @react-native-async-storage/async-storage
components/AufgabeZeile.js
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';

function AufgabeZeile({ aufgabe, onUmschalten, onLoeschen }) {
  return (
    <View style={styles.zeile}>
      <TouchableOpacity style={styles.textBereich} onPress={() => onUmschalten(aufgabe.id)}>
        <View style={[styles.checkbox, aufgabe.erledigt && styles.checkboxAktiv]}>
          {aufgabe.erledigt && <Text style={styles.haken}>✓</Text>}
        </View>
        <Text style={[styles.text, aufgabe.erledigt && styles.textErledigt]}>
          {aufgabe.text}
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onLoeschen(aufgabe.id)}>
        <Text style={styles.loeschen}>✕</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  zeile: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#e5e7eb' },
  textBereich: { flexDirection: 'row', alignItems: 'center', flex: 1 },
  checkbox: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: '#9ca3af', alignItems: 'center', justifyContent: 'center', marginRight: 10 },
  checkboxAktiv: { backgroundColor: '#16a34a', borderColor: '#16a34a' },
  haken: { color: 'white', fontSize: 12, fontWeight: 'bold' },
  text: { fontSize: 16, flexShrink: 1 },
  textErledigt: { textDecorationLine: 'line-through', color: '#9ca3af' },
  loeschen: { color: '#dc2626', fontSize: 16, paddingHorizontal: 8 },
});

export default AufgabeZeile;
App.js
import { useState, useEffect } from 'react';
import {
  View, Text, TextInput, TouchableOpacity, FlatList, StyleSheet,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import AufgabeZeile from './components/AufgabeZeile';

const SPEICHER_SCHLUESSEL = '@todo_app_aufgaben';
const FILTER = ['Alle', 'Offen', 'Erledigt'];

export default function App() {
  const [aufgaben, setAufgaben] = useState([]);
  const [eingabe, setEingabe] = useState('');
  const [filter, setFilter] = useState('Alle');

  useEffect(() => {
    AsyncStorage.getItem(SPEICHER_SCHLUESSEL).then((gespeichert) => {
      if (gespeichert) setAufgaben(JSON.parse(gespeichert));
    });
  }, []);

  useEffect(() => {
    AsyncStorage.setItem(SPEICHER_SCHLUESSEL, JSON.stringify(aufgaben));
  }, [aufgaben]);

  function hinzufuegen() {
    const text = eingabe.trim();
    if (!text) return;
    setAufgaben((liste) => [...liste, { id: Date.now(), text, erledigt: false }]);
    setEingabe('');
  }

  function umschalten(id) {
    setAufgaben((liste) =>
      liste.map((a) => (a.id === id ? { ...a, erledigt: !a.erledigt } : a))
    );
  }

  function loeschen(id) {
    setAufgaben((liste) => liste.filter((a) => a.id !== id));
  }

  const gefiltert = aufgaben.filter((a) => {
    if (filter === 'Offen') return !a.erledigt;
    if (filter === 'Erledigt') return a.erledigt;
    return true;
  });

  const offenAnzahl = aufgaben.filter((a) => !a.erledigt).length;

  return (
    <View style={styles.container}>
      <Text style={styles.titel}>Meine Aufgaben</Text>

      <View style={styles.eingabeZeile}>
        <TextInput
          style={styles.eingabe}
          placeholder="Neue Aufgabe…"
          value={eingabe}
          onChangeText={setEingabe}
          onSubmitEditing={hinzufuegen}
          returnKeyType="done"
        />
        <TouchableOpacity style={styles.hinzufuegenButton} onPress={hinzufuegen}>
          <Text style={styles.hinzufuegenText}>+</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.filterZeile}>
        {FILTER.map((f) => (
          <TouchableOpacity
            key={f}
            style={[styles.filterButton, filter === f && styles.filterButtonAktiv]}
            onPress={() => setFilter(f)}
          >
            <Text style={[styles.filterText, filter === f && styles.filterTextAktiv]}>
              {f}
            </Text>
          </TouchableOpacity>
        ))}
      </View>

      <FlatList
        data={gefiltert}
        keyExtractor={(item) => String(item.id)}
        renderItem={({ item }) => (
          <AufgabeZeile aufgabe={item} onUmschalten={umschalten} onLoeschen={loeschen} />
        )}
        ListEmptyComponent={
          <Text style={styles.leer}>Keine Aufgaben in dieser Ansicht.</Text>
        }
      />

      <Text style={styles.zaehler}>{offenAnzahl} offene Aufgabe(n)</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, paddingTop: 60, paddingHorizontal: 20 },
  titel: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
  eingabeZeile: { flexDirection: 'row', marginBottom: 12 },
  eingabe: { flex: 1, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, padding: 10, marginRight: 8 },
  hinzufuegenButton: { width: 44, height: 44, borderRadius: 8, backgroundColor: '#2563eb', alignItems: 'center', justifyContent: 'center' },
  hinzufuegenText: { color: 'white', fontSize: 22, lineHeight: 24 },
  filterZeile: { flexDirection: 'row', gap: 8, marginBottom: 12 },
  filterButton: { paddingVertical: 6, paddingHorizontal: 14, borderRadius: 16, backgroundColor: '#f3f4f6' },
  filterButtonAktiv: { backgroundColor: '#2563eb' },
  filterText: { color: '#374151' },
  filterTextAktiv: { color: 'white', fontWeight: 'bold' },
  leer: { textAlign: 'center', marginTop: 32, color: '#9ca3af' },
  zaehler: { textAlign: 'center', marginTop: 12, color: '#6b7280' },
});

4. Erklärung

  • ZWEI getrennte useEffect-Hooks: der erste (leeres Dependency-Array) lädt EINMALIG beim Start aus AsyncStorage, der zweite (Dependency [aufgaben]) speichert bei JEDER Änderung automatisch – kein manueller Speichern-Button nötig.
  • gefiltert ist eine ABGELEITETE Variable, kein eigener State – sie wird bei jedem Render aus aufgaben und filter neu berechnet, wodurch nie eine Inkonsistenz zwischen Original- und gefilterten Daten entstehen kann.
  • onSubmitEditing={{hinzufuegen}} erlaubt das Hinzufügen per Tastatur-"Fertig"-Taste, zusätzlich zum "+"-Button – zwei Wege zum selben Ziel, ein gängiges UX-Muster.
  • Date.now() als id ist für diese einfache App ausreichend eindeutig (zwei Aufgaben können praktisch nie in derselben Millisekunde erstellt werden); für Produktionscode mit garantierter Eindeutigkeit wäre expo-cryptos randomUUID() vorzuziehen.

5. Ausgaben

Ausgabe
Titel "Meine Aufgaben", darunter ein Eingabefeld mit blauem "+"-Button, drei Filter-Chips ("Alle" aktiv/blau), darunter die Aufgabenliste mit Kreis-Checkbox links (grün mit Haken bei Erledigt, durchgestrichener Text) und rotem "✕" rechts, ganz unten "3 offene Aufgabe(n)". Nach App-Neustart bleiben alle Aufgaben erhalten (persistiert).