ToDo App in React Native
ToDo App
~12 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
The ToDo app is the classic "Hello World" of more complex mobile apps – it bundles almost everything from this tutorial into one realistic project: a list, input, state, persistence, conditional rendering, and deletion.
1. Description
This app manages a list of tasks (objects with id, text, done), persists it with AsyncStorage (see the chapter of the same name), and offers adding, marking done, deleting, and a filter (All/Active/Completed).
2. Short example
const [tasks, setTasks] = useState([]);
function addTask(text) {
setTasks((list) => [...list, { id: Date.now(), text, done: false }]);
}3. Complete project: a persistent 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 TaskRow({ task, onToggle, onDelete }) {
return (
<View style={styles.row}>
<TouchableOpacity style={styles.textArea} onPress={() => onToggle(task.id)}>
<View style={[styles.checkbox, task.done && styles.checkboxActive]}>
{task.done && <Text style={styles.check}>✓</Text>}
</View>
<Text style={[styles.text, task.done && styles.textDone]}>
{task.text}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => onDelete(task.id)}>
<Text style={styles.delete}>✕</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#e5e7eb' },
textArea: { flexDirection: 'row', alignItems: 'center', flex: 1 },
checkbox: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: '#9ca3af', alignItems: 'center', justifyContent: 'center', marginRight: 10 },
checkboxActive: { backgroundColor: '#16a34a', borderColor: '#16a34a' },
check: { color: 'white', fontSize: 12, fontWeight: 'bold' },
text: { fontSize: 16, flexShrink: 1 },
textDone: { textDecorationLine: 'line-through', color: '#9ca3af' },
delete: { color: '#dc2626', fontSize: 16, paddingHorizontal: 8 },
});
export default TaskRow;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 TaskRow from './components/TaskRow';
const STORAGE_KEY = '@todo_app_tasks';
const FILTERS = ['All', 'Active', 'Completed'];
export default function App() {
const [tasks, setTasks] = useState([]);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('All');
useEffect(() => {
AsyncStorage.getItem(STORAGE_KEY).then((stored) => {
if (stored) setTasks(JSON.parse(stored));
});
}, []);
useEffect(() => {
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
}, [tasks]);
function addTask() {
const text = input.trim();
if (!text) return;
setTasks((list) => [...list, { id: Date.now(), text, done: false }]);
setInput('');
}
function toggleTask(id) {
setTasks((list) =>
list.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
}
function deleteTask(id) {
setTasks((list) => list.filter((t) => t.id !== id));
}
const filtered = tasks.filter((t) => {
if (filter === 'Active') return !t.done;
if (filter === 'Completed') return t.done;
return true;
});
const activeCount = tasks.filter((t) => !t.done).length;
return (
<View style={styles.container}>
<Text style={styles.title}>My Tasks</Text>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
placeholder="New task…"
value={input}
onChangeText={setInput}
onSubmitEditing={addTask}
returnKeyType="done"
/>
<TouchableOpacity style={styles.addButton} onPress={addTask}>
<Text style={styles.addText}>+</Text>
</TouchableOpacity>
</View>
<View style={styles.filterRow}>
{FILTERS.map((f) => (
<TouchableOpacity
key={f}
style={[styles.filterButton, filter === f && styles.filterButtonActive]}
onPress={() => setFilter(f)}
>
<Text style={[styles.filterText, filter === f && styles.filterTextActive]}>
{f}
</Text>
</TouchableOpacity>
))}
</View>
<FlatList
data={filtered}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => (
<TaskRow task={item} onToggle={toggleTask} onDelete={deleteTask} />
)}
ListEmptyComponent={
<Text style={styles.empty}>No tasks in this view.</Text>
}
/>
<Text style={styles.counter}>{activeCount} task(s) left</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, paddingTop: 60, paddingHorizontal: 20 },
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
inputRow: { flexDirection: 'row', marginBottom: 12 },
input: { flex: 1, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, padding: 10, marginRight: 8 },
addButton: { width: 44, height: 44, borderRadius: 8, backgroundColor: '#2563eb', alignItems: 'center', justifyContent: 'center' },
addText: { color: 'white', fontSize: 22, lineHeight: 24 },
filterRow: { flexDirection: 'row', gap: 8, marginBottom: 12 },
filterButton: { paddingVertical: 6, paddingHorizontal: 14, borderRadius: 16, backgroundColor: '#f3f4f6' },
filterButtonActive: { backgroundColor: '#2563eb' },
filterText: { color: '#374151' },
filterTextActive: { color: 'white', fontWeight: 'bold' },
empty: { textAlign: 'center', marginTop: 32, color: '#9ca3af' },
counter: { textAlign: 'center', marginTop: 12, color: '#6b7280' },
});4. Explanation
- TWO separate
useEffecthooks: the first (empty dependency array) loads ONCE on startup fromAsyncStorage, the second (dependency[tasks]) automatically saves on EVERY change – no manual save button needed. filteredis a DERIVED variable, not its own state – it's recomputed fromtasksandfilteron every render, so an inconsistency between the original and filtered data can never occur.onSubmitEditing={{addTask}}allows adding via the keyboard's "done" key, in addition to the "+" button – two paths to the same result, a common UX pattern.Date.now()as theidis sufficiently unique for this simple app (two tasks can practically never be created in the same millisecond); for production code needing guaranteed uniqueness,expo-crypto'srandomUUID()would be preferable.