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

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-storage
components/TaskRow.js
import { 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;
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 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 useEffect hooks: the first (empty dependency array) loads ONCE on startup from AsyncStorage, the second (dependency [tasks]) automatically saves on EVERY change – no manual save button needed.
  • filtered is a DERIVED variable, not its own state – it's recomputed from tasks and filter on 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 the id is 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's randomUUID() would be preferable.

5. Outputs

Ausgabe
Title "My Tasks", below it an input field with a blue "+" button, three filter chips ("All" active/blue), below that the task list with a circular checkbox on the left (green with a checkmark when done, struck-through text) and a red "✕" on the right, at the very bottom "3 task(s) left". After an app restart, all tasks remain (persisted).