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-storageimport { 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;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 ausAsyncStorage, der zweite (Dependency[aufgaben]) speichert bei JEDER Änderung automatisch – kein manueller Speichern-Button nötig. gefiltertist eine ABGELEITETE Variable, kein eigener State – sie wird bei jedem Render ausaufgabenundfilterneu 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()alsidist 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äreexpo-cryptosrandomUUID()vorzuziehen.