State and events
State in React Native works the same as on the web: useState keeps the data, event handlers change it, and the screen re-renders. Let us combine the previous lessons into a working to-do list.
The task type and state
Each task is an object with an id, a title and a done flag. Rewrite app/index.tsx:
// app/index.tsx
import { useState } from 'react';
import { Button, StyleSheet, Text, TextInput, View } from 'react-native';
type Task = { id: string; title: string; done: boolean };
export default function Index() {
const [tasks, setTasks] = useState<Task[]>([]);
const [title, setTitle] = useState('');
function addTask() {
if (title.trim() === '') return;
setTasks([...tasks, { id: Date.now().toString(), title: title.trim(), done: false }]);
setTitle('');
}
return (
<View style={styles.screen}>
<TextInput
style={styles.input}
value={title}
onChangeText={setTitle}
placeholder="New task"
onSubmitEditing={addTask}
/>
<Button title="Add" onPress={addTask} />
{tasks.map((task) => (
<Text key={task.id} style={styles.task}>
{task.title}
</Text>
))}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, gap: 8 },
input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12 },
task: { fontSize: 18, paddingVertical: 6 },
});Type "Buy bread" and press "Add" or the return key on the keyboard: the line appears under the button and the field is cleared. An empty title is not added.
Marking a task done
Wrap the line in Pressable: a short press toggles done. Add the function inside the component and replace the list output:
// app/index.tsx
function toggleTask(id: string) {
setTasks(tasks.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
{tasks.map((task) => (
<Pressable key={task.id} onPress={() => toggleTask(task.id)}>
<Text style={[styles.task, task.done && styles.done]}>{task.title}</Text>
</Pressable>
))}Add Pressable to the import from react-native and the style done: { color: '#999', textDecorationLine: 'line-through' }. Pressing a task strikes it through, pressing again removes the mark.
Deleting with confirmation
On a phone it is customary to ask before deleting. Alert.alert takes a title, a message and an array of buttons:
// app/index.tsx
function confirmDelete(task: Task) {
Alert.alert('Delete task?', task.title, [
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: () => setTasks(tasks.filter((t) => t.id !== task.id)),
},
]);
}Pass it to Pressable: onLongPress={() => confirmDelete(task)}. Hold your finger on a task and a dialog with "Cancel" and "Delete" buttons appears.
Common mistake
Changing the state array directly:
// app/index.tsx
// Wrong:
tasks.push({ id: '1', title, done: false });
setTasks(tasks);React gets the same array and does not re-render, so the task does not appear. Always create a new array with [...tasks, newTask], map or filter, as covered in the React course.