Build a Todo List App with Calendar View

Build a mobile todo app with a calendar view using React Native and Expo.

Introduction

30 Second Summary

Everyone keeps a to-do list somewhere. Maybe it's scattered across sticky notes, random apps, or just floating around in your head.

In this project, you will build a mobile todo list app with a calendar view using React Native and Expo. You'll add tasks with due dates, see them on an interactive calendar, and mark them complete right from your phone.

What You'll Build

A React Native mobile app that manages tasks with due dates, displays them on a calendar, and persists data locally on your device.

By the end of this project, you'll have:

  • A working mobile app running on your physical phone with a task input form and date picker.
  • A calendar view that highlights days with tasks and lets you tap a day to see what's due.
  • Persistent local storage so your tasks survive app restarts.
  • Secret Mission: Add scheduled local notification reminders that alert you when a task is due.

Are there any prerequisites?

None. This guide walks you through installing everything you need, including Node.js, Expo, and the Expo Go app on your phone. No prior mobile development experience is required.

Set Up Your Development Environment

To build a mobile app, you need a development environment that connects your computer to your phone. React Native apps run on your device through a companion app called Expo Go, which loads your code instantly over your local network.

In this step, you will clean up the default project boilerplate and configure a minimal starting point for your todo app.

In this step, get ready to:
  • Verify your Node.js installation.
  • Reset the Expo project to a clean starting point.
  • Configure your app files and verify the app loads on your phone.
Verify your Node.js installation

Node.js powers the Expo development server that sends your code to your phone. Let's confirm it's working.

  • Open your terminal in VS Code by pressing Ctrl+` (macOS) or Ctrl+` (Windows).
  • Check your Node.js version by running this command:
node --version

You should see a version number like v24.15.0 or higher.

Don't see a version number?

If you see "command not found", Node.js is not installed or not in your system PATH. Download the LTS version from nodejs.org and restart your terminal after installation.

Help me troubleshoot my Node.js installation

Reset the project boilerplate

The default Expo template comes with demo screens and navigation examples. We want a clean slate to build our own todo app from scratch.

  • Navigate into your project folder in the terminal (the folder created by create-expo-app).
  • Remove the boilerplate code by running this command:
npm run reset-project

What does this command do?

This script moves the default demo code into a folder called app-example and creates a fresh app directory with just two files: index.tsx and _layout.tsx.

These two files are all you need to start building. You can safely delete the app-example folder later.

The terminal will ask you to confirm. Press Enter or type Y to proceed.

  • Verify the reset worked by running:
ls app

You should see index.tsx and _layout.tsx listed in the output.

Don't see these two files?

Make sure you ran the command from your project's root folder (the one containing package.json). If the reset didn't work, try running npm run reset-project again.

Help me fix my project reset

Configure your app's starting files

The reset gives you placeholder files. Let's replace them with the exact starting code our todo app needs.

  • Open app/index.tsx in VS Code (click the file in the Explorer sidebar on the left).
  • Replace everything in the file with this code:
import { View, Text } from 'react-native';

export default function Index() {
  return (
    <View>
      <Text>Hello, Expo!</Text>
    </View>
  );
}

What does this code do?

  • View is the basic container in React Native, similar to a div in web development.
  • Text displays text on screen. In React Native, all text must be wrapped in a Text component.
  • The Index function is your app's home screen.
  • Save app/index.tsx.
  • Now open app/_layout.tsx and replace everything with this code:
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="index" />
    </Stack>
  );
}

What does this code do?

  • Stack from expo-router sets up a navigation stack (screens that layer on top of each other).
  • headerShown: false hides the default navigation header so we have full control over our screen layout.
  • Stack.Screen name="index" registers app/index.tsx as the home screen.
  • Save app/_layout.tsx.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full content of each file at this point:

import { View, Text } from 'react-native';

export default function Index() {
  return (
    <View>
      <Text>Hello, Expo!</Text>
    </View>
  );
}
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="index" />
    </Stack>
  );
}
Verify the app on your phone

Your Expo dev server sends code changes to your phone instantly. Let's confirm the new code is working.

  • Return to your terminal and start the dev server by running:
npx expo start

What should I see?

Your terminal displays a QR code along with options for opening the app. On iOS, scan the QR code with your default Camera app. On Android, open the Expo Go app and tap Scan QR code.

  • Open the app on your phone using the QR code.
  • Confirm you see the text "Hello, Expo!" on your phone screen.

Don't see Hello, Expo! on your phone?

Make sure your phone and computer are on the same Wi-Fi network. If the app shows an error, check that both app/index.tsx and app/_layout.tsx are saved.

If you see the old demo content, try shaking your phone and pressing Reload from the developer menu.

Help me connect Expo Go to my development server

Your development environment is ready. Next up, you will build the task input form that lets users add todos with due dates.

Build the Task Input Form

Your app is running on your phone, but it only shows a static greeting. To build a useful todo app, you need a way for users to type in tasks and pick due dates.

In this step, you will create an input form with three parts: a text field for the task name, a date picker button, and an add button. Together, these let users create tasks with specific due dates.

In this step, get ready to:
  • Create a text input for the task title.
  • Add a date picker so users can select a due date.
  • Add a button that saves the task to the app's state.
Create a text input for the task title

Before writing the form, you need to install a package that provides the native date picker on iOS and Android. Expo includes this package in Expo Go, but you still need to register it in your project.

  • Install the date picker package by running this command in your terminal:
npx expo install @react-native-community/datetimepicker

Seeing an install error?

Always use npx expo install instead of npm install for Expo packages. The expo install command automatically picks the version compatible with your Expo SDK.

If you still see errors, try deleting node_modules and package-lock.json, then running npm install followed by the expo install command again.

help me fix my @react-native-community/datetimepicker install error

Now you will replace the boilerplate in app/index.tsx with the foundation for your task form. This includes imports, a Task interface to define the shape of each task, and React state hooks to track user input.

  • Open app/index.tsx in VS Code.
  • Select all the existing code and replace it with the following:
import { useState } from 'react';
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  Platform,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';

// Define the shape of a task object
interface Task {
  id: string;
  title: string;
  date: string;
  completed: boolean;
}

// Key for saving tasks to local storage later
const STORAGE_KEY = '@todo_tasks';

export default function Index() {
  // State to hold all tasks
  const [tasks, setTasks] = useState<Task[]>([]);
  // State for the text input value
  const [taskTitle, setTaskTitle] = useState('');
  // State for which calendar day is selected
  const [selectedDate, setSelectedDate] = useState(
    new Date().toISOString().split('T')[0]
  );
  // State for the date picker value
  const [dueDate, setDueDate] = useState(new Date());
  // State to show/hide the date picker
  const [showDatePicker, setShowDatePicker] = useState(false);

  return (
    <View style={styles.container}>
      <Text style={styles.header}>My Tasks</Text>
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          value={taskTitle}
          onChangeText={setTaskTitle}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    paddingTop: 60,
    paddingHorizontal: 16,
  },
  header: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 16,
    color: '#333',
  },
  inputRow: {
    flexDirection: 'row',
    marginBottom: 16,
    gap: 8,
  },
  input: {
    flex: 1,
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#ddd',
  },
});

What does this code do?

  • The Task interface defines what each task looks like: an ID, a title, a date string, and a completed flag.
  • Five useState hooks track the task list, the text input, the selected calendar day, the date picker value, and whether the date picker is visible.
  • The JSX renders a styled container with a "My Tasks" header and a TextInput where users type task names.
  • The StyleSheet at the bottom keeps your styling organized and performant in React Native.
  • Save app/index.tsx.
  • Check your phone. You should see a "My Tasks" header and a white text input field with placeholder text "Add a new task...".

Not seeing the text input?

Make sure you saved the file. The Expo dev server hot-reloads changes automatically.

If your app shows a red error screen, check for typos in the import statements. The import from @react-native-community/datetimepicker requires the package to be installed first.

help me fix the error in my React Native task input form

Add a date picker so users can select a due date

The text input lets users name their task, but they also need to pick when it is due. You will add a date button that shows the selected due date and opens a native date picker when tapped.

  • In app/index.tsx, find the closing curly brace of the Index component (the line with } just before const styles).
  • Add the following function just above the return statement:
  const onDatePickerChange = (event: any, date?: Date) => {
    setShowDatePicker(Platform.OS === 'ios');
    if (date) {
      setDueDate(date);
    }
  };

What does this function do?

On Android, the date picker closes automatically after selection. On iOS, it stays open. The Platform.OS === 'ios' check handles this difference by keeping showDatePicker true only on iOS.

  • Now find the TextInput closing tag (/>) inside the inputRow View.
  • Add the date button directly after the TextInput (still inside the inputRow View):
        <TouchableOpacity
          style={styles.dateButton}
          onPress={() => setShowDatePicker(true)}
        >
          <Text style={styles.dateButtonText}>
            {dueDate.toLocaleDateString()}
          </Text>
        </TouchableOpacity>
  • Next, find the closing </View> tag for the inputRow.
  • Add the conditional DateTimePicker directly after that closing tag (but still inside the outer container View):
      {showDatePicker && (
        <DateTimePicker
          value={dueDate}
          mode="date"
          display="default"
          onChange={onDatePickerChange}
          minimumDate={new Date()}
        />
      )}
  • Finally, add these two styles inside your StyleSheet.create object, after the input style:
  dateButton: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    justifyContent: 'center',
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButtonText: {
    fontSize: 12,
    color: '#666',
  },
  • Save app/index.tsx.
  • Check your phone. You should see a small white button next to the text input showing today's date.
  • Tap the date button. A native date picker should appear (a spinning wheel on iOS, a calendar dialog on Android).

Date picker not showing up?

Make sure the onDatePickerChange function is placed inside the Index component (after the state hooks, before the return statement).

Check that the DateTimePicker block is outside the inputRow View but inside the container View.

help me fix my date picker not appearing in my React Native app

Add a button that saves the task to the app's state

You can type a task name and pick a date, but nothing happens yet. You need a function to create a new task object and an add button to trigger it.

  • In app/index.tsx, add the addTask function just above the onDatePickerChange function:
  const addTask = () => {
    if (!taskTitle.trim()) return;
    const newTask: Task = {
      id: Date.now().toString(),
      title: taskTitle.trim(),
      date: dueDate.toISOString().split('T')[0],
      completed: false,
    };
    setTasks((prev) => [...prev, newTask]);
    setTaskTitle('');
    setDueDate(new Date());
  };

What does addTask do?

It first checks if the input is empty (preventing blank tasks). It creates a new task with a unique ID using Date.now(), the trimmed title, and the formatted due date.

After adding the task to the list, it resets the input field and date picker back to defaults so the user can immediately add another task.

  • Now update the TextInput to also submit when the user presses Enter on their keyboard. Find your TextInput and add the onSubmitEditing prop:
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          value={taskTitle}
          onChangeText={setTaskTitle}
          onSubmitEditing={addTask}
        />
  • Next, add the "+" button after the date button's closing TouchableOpacity tag (still inside the inputRow View):
        <TouchableOpacity style={styles.addButton} onPress={addTask}>
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
  • Add these two styles inside your StyleSheet.create object, after the dateButtonText style:
  addButton: {
    backgroundColor: '#2196F3',
    borderRadius: 8,
    width: 44,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
  • Save app/index.tsx.
  • Check your phone. You should see a blue "+" button to the right of the date button.
  • Type a task name (like "Buy groceries") and tap the "+" button. The text input should clear, confirming the task was saved to state.

Input not clearing when you tap +?

Make sure the addTask function is inside the Index component, not outside it.

Check that onPress={addTask} on the add button references the function without parentheses. Writing onPress={addTask()} would call it immediately instead of on tap.

help me figure out why my add button isn't working in my React Native todo app

✔️ Awesome, I've got everything!

Great. Double check you have saved app/index.tsx and your app shows the full input row on your phone.

ⓧ I'd like to double check the full code

Here is the complete app/index.tsx at the end of this step:

import { useState } from 'react';
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  Platform,
} from 'react-native';
import DateTimePicker from '@react-native-community/datetimepicker';

interface Task {
  id: string;
  title: string;
  date: string;
  completed: boolean;
}

const STORAGE_KEY = '@todo_tasks';

export default function Index() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [taskTitle, setTaskTitle] = useState('');
  const [selectedDate, setSelectedDate] = useState(
    new Date().toISOString().split('T')[0]
  );
  const [dueDate, setDueDate] = useState(new Date());
  const [showDatePicker, setShowDatePicker] = useState(false);

  const addTask = () => {
    if (!taskTitle.trim()) return;
    const newTask: Task = {
      id: Date.now().toString(),
      title: taskTitle.trim(),
      date: dueDate.toISOString().split('T')[0],
      completed: false,
    };
    setTasks((prev) => [...prev, newTask]);
    setTaskTitle('');
    setDueDate(new Date());
  };

  const onDatePickerChange = (event: any, date?: Date) => {
    setShowDatePicker(Platform.OS === 'ios');
    if (date) {
      setDueDate(date);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.header}>My Tasks</Text>
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          value={taskTitle}
          onChangeText={setTaskTitle}
          onSubmitEditing={addTask}
        />
        <TouchableOpacity
          style={styles.dateButton}
          onPress={() => setShowDatePicker(true)}
        >
          <Text style={styles.dateButtonText}>
            {dueDate.toLocaleDateString()}
          </Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.addButton} onPress={addTask}>
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>
      {showDatePicker && (
        <DateTimePicker
          value={dueDate}
          mode="date"
          display="default"
          onChange={onDatePickerChange}
          minimumDate={new Date()}
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    paddingTop: 60,
    paddingHorizontal: 16,
  },
  header: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 16,
    color: '#333',
  },
  inputRow: {
    flexDirection: 'row',
    marginBottom: 16,
    gap: 8,
  },
  input: {
    flex: 1,
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButton: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    justifyContent: 'center',
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButtonText: {
    fontSize: 12,
    color: '#666',
  },
  addButton: {
    backgroundColor: '#2196F3',
    borderRadius: 8,
    width: 44,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
});

Your input form is complete. Users can type a task, pick a date, and tap "+" to save it. Next up, you will display these tasks on a calendar so users can see which days have tasks at a glance.

Display Tasks in a Calendar View

Your task input form is working. You can type a task title, pick a due date, and add it to your app's state.

The problem is that you can't actually see your tasks yet. In this step, you'll install a calendar component that displays a full monthly view with dots on days that have tasks. Tapping a day filters the list below to show only that day's tasks.

In this step, get ready to:
  • Install and configure the react-native-calendars package.
  • Show a calendar with marked dates for days that have tasks.
  • Display a filtered task list below the calendar for the selected day.
Install and configure react-native-calendars

The react-native-calendars package gives you a ready-made Calendar component with month navigation, day selection, and date marking built in. It's pure JavaScript, so it works directly with Expo Go without any extra native setup.

  • Return to your terminal and stop the dev server by pressing Ctrl+C.
  • Install the calendar package by running this command:
npx expo install react-native-calendars
  • Restart the dev server by running:
npx expo start

Why npx expo install instead of npm install?

The npx expo install command automatically picks a version of the package that's compatible with your current Expo SDK. This prevents version mismatch errors that are common in React Native projects.

Seeing install errors?

Make sure you stopped the dev server before running the install command. If you see network errors, check your internet connection and try running the install command again.

help me fix my react-native-calendars install error

Now update your imports at the top of app/index.tsx to bring in the new tools you'll need for this step.

  • In app/index.tsx, find the first line:
import { useState } from 'react';
  • Replace it with this line to add useCallback:
import { useState, useCallback } from 'react';
  • Find the react-native import block:
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  Platform,
} from 'react-native';
  • Replace it with this updated version that adds FlatList:
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  Platform,
} from 'react-native';
  • Add this new import line directly below the react-native import block:
import { Calendar } from 'react-native-calendars';

What are these new imports for?

You added three things: useCallback (a React hook that memoizes functions so the calendar doesn't re-render unnecessarily), FlatList (a performant scrollable list component), and Calendar (the monthly calendar component from the package you just installed).

Add the calendar with marked dates

The calendar needs to know which dates have tasks so it can show dots on those days. You'll create a function that builds this mapping from your task list.

  • In app/index.tsx, find the onDatePickerChange function. Add the following getMarkedDates function directly below it:
  const getMarkedDates = useCallback(() => {
    const marks: Record<string, { marked: boolean; dotColor: string }> = {};
    tasks.forEach((task) => {
      if (!task.completed) {
        marks[task.date] = { marked: true, dotColor: '#4CAF50' };
      }
    });
    marks[selectedDate] = {
      ...marks[selectedDate],
      selected: true,
      selectedColor: '#2196F3',
    } as any;
    return marks;
  }, [tasks, selectedDate]);

What does this code do?

The getMarkedDates function loops through all your tasks and builds an object that the Calendar component understands. Each incomplete task's date gets a green dot. The currently selected date gets a blue highlight.

Wrapping it in useCallback means React only recalculates this object when tasks or selectedDate change. This keeps the calendar from doing unnecessary work on every render.

Now add the Calendar component to your app's screen. It goes after the date picker in the JSX.

  • Find the closing tag of the DateTimePicker conditional block. It looks like this:
      {showDatePicker && (
        <DateTimePicker
          value={dueDate}
          mode="date"
          display="default"
          onChange={onDatePickerChange}
          minimumDate={new Date()}
        />
      )}
  • Add the following Calendar component directly below that block:
      <Calendar
        markedDates={getMarkedDates()}
        onDayPress={(day: { dateString: string }) =>
          setSelectedDate(day.dateString)
        }
        theme={{
          todayTextColor: '#2196F3',
          selectedDayBackgroundColor: '#2196F3',
          dotColor: '#4CAF50',
        }}
      />

What does this Calendar component do?

The markedDates prop passes your task-date mapping so dots appear on task days. The onDayPress callback updates selectedDate when the user taps a day. The theme prop customizes colors for today's date, the selected day highlight, and the dot indicators.

  • Save app/index.tsx.
  • Check your phone. You should see a full monthly calendar appear below the task input row.

Calendar not showing up?

Make sure you added the Calendar import line at the top of the file. Without the import, the component is undefined and nothing renders.

If the app shows a red error screen, check that the Calendar JSX is placed inside the outermost <View> but after the DateTimePicker block.

help me fix my Calendar component not rendering in React Native

Display a filtered task list below the calendar

When a user taps a day on the calendar, they need to see the tasks due on that specific day. You'll filter the task array by the selected date and display the results in a scrollable list.

  • In app/index.tsx, find the getMarkedDates function you just added. Add this line directly below it (before the return statement):
  const filteredTasks = tasks.filter((task) => task.date === selectedDate);
  • Now add the task list UI. Find the <Calendar /> component you just added, and paste this code directly below it:
      <Text style={styles.dateLabel}>Tasks for {selectedDate}</Text>

      <FlatList
        data={filteredTasks}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.taskItem}>
            <Text style={styles.taskText}>{item.title}</Text>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>No tasks for this day</Text>
        }
      />

What does this code do?

The filteredTasks line creates a new array containing only the tasks whose date matches the day you tapped on the calendar.

The FlatList component efficiently renders this filtered array as a scrollable list. The ListEmptyComponent prop shows a friendly message when there are no tasks for the selected day.

  • Finally, add the styles these new elements need. Find your styles object at the bottom of the file. Add these four new style definitions after the existing addButtonText style:
  dateLabel: {
    fontSize: 16,
    fontWeight: '600',
    marginTop: 16,
    marginBottom: 8,
    color: '#555',
  },
  taskItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
  },
  taskText: {
    fontSize: 16,
    color: '#333',
  },
  emptyText: {
    textAlign: 'center',
    color: '#999',
    marginTop: 20,
    fontSize: 14,
  },

What do these styles do?

The dateLabel style makes the "Tasks for ..." heading stand out with slightly larger, bolder text. Each taskItem is a white card with rounded corners. The emptyText style centers the "No tasks for this day" placeholder.

  • Save app/index.tsx.
  • Check your phone. Below the calendar, you should see "Tasks for" followed by today's date, and a grey "No tasks for this day" message.
  • Try adding a task using the input form at the top. After adding it, you should see a green dot appear on the calendar for that task's date.
  • Tap the dotted date on the calendar. The task list below updates to show your task for that day.

Not seeing dots on the calendar?

Make sure your getMarkedDates function is placed outside the return statement but inside the Index component function. If it's inside the JSX, it won't work.

Also check that you have useCallback in your React import at the top of the file. A missing import causes a runtime error.

help me debug why calendar dots aren't appearing for my tasks

✔️ Awesome, I've got everything!

Great. Double check you've saved app/index.tsx and your app is rendering the calendar with the task list below.

ⓧ I'd like to double check the full code

Here is the complete app/index.tsx at this point:

import { useState, useCallback } from 'react';
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  Platform,
} from 'react-native';
import { Calendar } from 'react-native-calendars';
import DateTimePicker from '@react-native-community/datetimepicker';

interface Task {
  id: string;
  title: string;
  date: string;
  completed: boolean;
}

const STORAGE_KEY = '@todo_tasks';

export default function Index() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [taskTitle, setTaskTitle] = useState('');
  const [selectedDate, setSelectedDate] = useState(
    new Date().toISOString().split('T')[0]
  );
  const [dueDate, setDueDate] = useState(new Date());
  const [showDatePicker, setShowDatePicker] = useState(false);

  const addTask = () => {
    if (!taskTitle.trim()) return;
    const newTask: Task = {
      id: Date.now().toString(),
      title: taskTitle.trim(),
      date: dueDate.toISOString().split('T')[0],
      completed: false,
    };
    setTasks((prev) => [...prev, newTask]);
    setTaskTitle('');
    setDueDate(new Date());
  };

  const onDatePickerChange = (event: any, date?: Date) => {
    setShowDatePicker(Platform.OS === 'ios');
    if (date) {
      setDueDate(date);
    }
  };

  const getMarkedDates = useCallback(() => {
    const marks: Record<string, { marked: boolean; dotColor: string }> = {};
    tasks.forEach((task) => {
      if (!task.completed) {
        marks[task.date] = { marked: true, dotColor: '#4CAF50' };
      }
    });
    marks[selectedDate] = {
      ...marks[selectedDate],
      selected: true,
      selectedColor: '#2196F3',
    } as any;
    return marks;
  }, [tasks, selectedDate]);

  const filteredTasks = tasks.filter((task) => task.date === selectedDate);

  return (
    <View style={styles.container}>
      <Text style={styles.header}>My Tasks</Text>
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          value={taskTitle}
          onChangeText={setTaskTitle}
          onSubmitEditing={addTask}
        />
        <TouchableOpacity
          style={styles.dateButton}
          onPress={() => setShowDatePicker(true)}
        >
          <Text style={styles.dateButtonText}>
            {dueDate.toLocaleDateString()}
          </Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.addButton} onPress={addTask}>
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>
      {showDatePicker && (
        <DateTimePicker
          value={dueDate}
          mode="date"
          display="default"
          onChange={onDatePickerChange}
          minimumDate={new Date()}
        />
      )}
      <Calendar
        markedDates={getMarkedDates()}
        onDayPress={(day: { dateString: string }) =>
          setSelectedDate(day.dateString)
        }
        theme={{
          todayTextColor: '#2196F3',
          selectedDayBackgroundColor: '#2196F3',
          dotColor: '#4CAF50',
        }}
      />
      <Text style={styles.dateLabel}>Tasks for {selectedDate}</Text>
      <FlatList
        data={filteredTasks}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.taskItem}>
            <Text style={styles.taskText}>{item.title}</Text>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>No tasks for this day</Text>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    paddingTop: 60,
    paddingHorizontal: 16,
  },
  header: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 16,
    color: '#333',
  },
  inputRow: {
    flexDirection: 'row',
    marginBottom: 16,
    gap: 8,
  },
  input: {
    flex: 1,
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButton: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    justifyContent: 'center',
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButtonText: {
    fontSize: 12,
    color: '#666',
  },
  addButton: {
    backgroundColor: '#2196F3',
    borderRadius: 8,
    width: 44,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
  dateLabel: {
    fontSize: 16,
    fontWeight: '600',
    marginTop: 16,
    marginBottom: 8,
    color: '#555',
  },
  taskItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
  },
  taskText: {
    fontSize: 16,
    color: '#333',
  },
  emptyText: {
    textAlign: 'center',
    color: '#999',
    marginTop: 20,
    fontSize: 14,
  },
});

Your app now has a working calendar view with interactive day selection and a filtered task list. Next up, you'll add the ability to mark tasks as complete and delete them.

Add Complete and Delete Functionality

Your app now displays tasks on a calendar and lists them by day. That is great for seeing what is due, but you cannot actually do anything with those tasks yet.

In this step, you will add the ability to mark tasks as complete and delete tasks you no longer need. These interactions make the app functional rather than just a read-only list.

In this step, get ready to:
  • Add toggle and delete functions to manage task state.
  • Update the task list with a checkbox and delete button.
  • Style the new controls with visual feedback for completed tasks.
Add the toggle and delete functions

Each task needs two actions: toggling its completed status, and removing it entirely. Both functions update the tasks state array.

  • In app/index.tsx, find the addTask function.
  • Add the following two functions directly below it:
  const toggleTask = (id: string) => {
    setTasks((prev) =>
      prev.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  const deleteTask = (id: string) => {
    setTasks((prev) => prev.filter((task) => task.id !== id));
  };

What does this code do?

These two functions handle user interaction with existing tasks:

  • toggleTask finds a task by its ID and flips the completed boolean. It uses map to return a new array with only that one task changed.
  • deleteTask uses filter to return a new array that excludes the task with the matching ID.

You will not see a change yet. The next chunk connects these functions to the task list UI.

Update the task list with interactive controls

Right now, each task just displays its title as plain text. You will replace that with a tappable checkbox area and a delete button.

  • In app/index.tsx, scroll down to the FlatList component.
  • Find the renderItem prop. It currently looks like this:
        renderItem={({ item }) => (
          <View style={styles.taskItem}>
            <Text style={styles.taskText}>{item.title}</Text>
          </View>
        )}
  • Replace that entire renderItem prop with this updated version:
        renderItem={({ item }) => (
          <View style={styles.taskItem}>
            <TouchableOpacity
              style={styles.taskContent}
              onPress={() => toggleTask(item.id)}
            >
              <View
                style={[
                  styles.checkbox,
                  item.completed && styles.checkboxChecked,
                ]}
              />
              <Text
                style={[
                  styles.taskText,
                  item.completed && styles.taskTextCompleted,
                ]}
              >
                {item.title}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={() => deleteTask(item.id)}>
              <Text style={styles.deleteButton}>✕</Text>
            </TouchableOpacity>
          </View>
        )}

What's happening here?

Each task row now has two touchable areas:

  • The left side (taskContent) wraps a circular checkbox and the task title. Tapping anywhere in this area calls toggleTask.
  • The right side is a small delete button showing ✕. Tapping it calls deleteTask.
  • The style array syntax ([styles.checkbox, item.completed && styles.checkboxChecked]) conditionally applies the filled-in style only when the task is complete.

You will not see the checkbox circle yet because it needs width and height styles. The next chunk adds those.

Add styles for the new controls

The new UI elements need styles to be visible and provide visual feedback when a task is completed.

  • In your styles object at the bottom of app/index.tsx, find the taskText style.
  • Add the following new styles directly above taskText:
  taskContent: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  checkbox: {
    width: 22,
    height: 22,
    borderRadius: 11,
    borderWidth: 2,
    borderColor: '#2196F3',
    marginRight: 12,
  },
  checkboxChecked: {
    backgroundColor: '#2196F3',
  },
  • Now find the taskText style and add these two styles directly below it:
  taskTextCompleted: {
    textDecorationLine: 'line-through',
    color: '#999',
  },
  deleteButton: {
    fontSize: 18,
    color: '#ff4444',
    padding: 4,
  },

What do these styles do?

  • taskContent uses flex: 1 to take up all available space, pushing the delete button to the right edge.
  • checkbox creates a 22x22 circle (borderRadius: 11 is half the width) with a blue border.
  • checkboxChecked fills the circle with blue when the task is complete.
  • taskTextCompleted adds a strikethrough line and grays out the text for completed tasks.
  • Save app/index.tsx.
  • On your phone, add a task if you do not have one already.
  • Tap the task row. You should see the circle fill in with blue and the task title gets a strikethrough with gray text.
  • Tap the red ✕ on the right. The task should disappear from the list.

Tasks not responding to taps?

  • Make sure your renderItem uses TouchableOpacity (not View) for the tappable areas. A View does not respond to touch events.
  • Check that toggleTask and deleteTask are defined inside the Index component function, not outside it.
  • If the checkbox circle is invisible, verify you added both width: 22 and height: 22 in the checkbox style.

Help me debug why tapping tasks is not working in my React Native app.

What about the calendar dots?

You may notice that completed tasks lose their green dot on the calendar. The getMarkedDates function you wrote in the previous step already checks if (!task.completed) before adding a dot. No extra code needed.

Try it: mark a task complete, then scroll the calendar to that task's date. The green dot disappears.

✔️ Awesome, I've got everything!

Great. Double check you have saved app/index.tsx before moving on.

ⓧ I'd like to double check the full code

import { useState, useCallback } from 'react';
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  Platform,
} from 'react-native';
import { Calendar } from 'react-native-calendars';
import DateTimePicker from '@react-native-community/datetimepicker';

interface Task {
  id: string;
  title: string;
  date: string;
  completed: boolean;
}

const STORAGE_KEY = '@todo_tasks';

export default function Index() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [taskTitle, setTaskTitle] = useState('');
  const [selectedDate, setSelectedDate] = useState(
    new Date().toISOString().split('T')[0]
  );
  const [dueDate, setDueDate] = useState(new Date());
  const [showDatePicker, setShowDatePicker] = useState(false);

  const addTask = () => {
    if (!taskTitle.trim()) return;
    const newTask: Task = {
      id: Date.now().toString(),
      title: taskTitle.trim(),
      date: dueDate.toISOString().split('T')[0],
      completed: false,
    };
    setTasks((prev) => [...prev, newTask]);
    setTaskTitle('');
    setDueDate(new Date());
  };

  const toggleTask = (id: string) => {
    setTasks((prev) =>
      prev.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  const deleteTask = (id: string) => {
    setTasks((prev) => prev.filter((task) => task.id !== id));
  };

  const onDatePickerChange = (event: any, date?: Date) => {
    setShowDatePicker(Platform.OS === 'ios');
    if (date) {
      setDueDate(date);
    }
  };

  const getMarkedDates = useCallback(() => {
    const marks: Record<string, { marked: boolean; dotColor: string }> = {};
    tasks.forEach((task) => {
      if (!task.completed) {
        marks[task.date] = { marked: true, dotColor: '#4CAF50' };
      }
    });
    marks[selectedDate] = {
      ...marks[selectedDate],
      selected: true,
      selectedColor: '#2196F3',
    } as any;
    return marks;
  }, [tasks, selectedDate]);

  const filteredTasks = tasks.filter((task) => task.date === selectedDate);

  return (
    <View style={styles.container}>
      <Text style={styles.header}>My Tasks</Text>
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          value={taskTitle}
          onChangeText={setTaskTitle}
          onSubmitEditing={addTask}
        />
        <TouchableOpacity
          style={styles.dateButton}
          onPress={() => setShowDatePicker(true)}
        >
          <Text style={styles.dateButtonText}>
            {dueDate.toLocaleDateString()}
          </Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.addButton} onPress={addTask}>
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>
      {showDatePicker && (
        <DateTimePicker
          value={dueDate}
          mode="date"
          display="default"
          onChange={onDatePickerChange}
          minimumDate={new Date()}
        />
      )}
      <Calendar
        markedDates={getMarkedDates()}
        onDayPress={(day: { dateString: string }) =>
          setSelectedDate(day.dateString)
        }
        theme={{
          todayTextColor: '#2196F3',
          selectedDayBackgroundColor: '#2196F3',
          dotColor: '#4CAF50',
        }}
      />
      <Text style={styles.dateLabel}>Tasks for {selectedDate}</Text>
      <FlatList
        data={filteredTasks}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.taskItem}>
            <TouchableOpacity
              style={styles.taskContent}
              onPress={() => toggleTask(item.id)}
            >
              <View
                style={[
                  styles.checkbox,
                  item.completed && styles.checkboxChecked,
                ]}
              />
              <Text
                style={[
                  styles.taskText,
                  item.completed && styles.taskTextCompleted,
                ]}
              >
                {item.title}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={() => deleteTask(item.id)}>
              <Text style={styles.deleteButton}>✕</Text>
            </TouchableOpacity>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>No tasks for this day</Text>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    paddingTop: 60,
    paddingHorizontal: 16,
  },
  header: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 16,
    color: '#333',
  },
  inputRow: {
    flexDirection: 'row',
    marginBottom: 16,
    gap: 8,
  },
  input: {
    flex: 1,
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButton: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    justifyContent: 'center',
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButtonText: {
    fontSize: 12,
    color: '#666',
  },
  addButton: {
    backgroundColor: '#2196F3',
    borderRadius: 8,
    width: 44,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
  dateLabel: {
    fontSize: 16,
    fontWeight: '600',
    marginTop: 16,
    marginBottom: 8,
    color: '#555',
  },
  taskItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
  },
  taskContent: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  checkbox: {
    width: 22,
    height: 22,
    borderRadius: 11,
    borderWidth: 2,
    borderColor: '#2196F3',
    marginRight: 12,
  },
  checkboxChecked: {
    backgroundColor: '#2196F3',
  },
  taskText: {
    fontSize: 16,
    color: '#333',
  },
  taskTextCompleted: {
    textDecorationLine: 'line-through',
    color: '#999',
  },
  deleteButton: {
    fontSize: 18,
    color: '#ff4444',
    padding: 4,
  },
  emptyText: {
    textAlign: 'center',
    color: '#999',
    marginTop: 20,
    fontSize: 14,
  },
});

Your tasks are now fully interactive. Next up, you will add local storage so your tasks survive when you close and reopen the app.

Persist Tasks with AsyncStorage

Your todo app now lets you add, complete, and delete tasks with a visual calendar. There is one big problem though: if you close the app and reopen it, all your tasks disappear.

Mobile apps need to remember data between sessions. In this step, you will add local storage so your tasks survive app restarts.

In this step, get ready to:
  • Install the AsyncStorage package.
  • Save tasks to local storage whenever the task list changes.
  • Load saved tasks from local storage when the app starts.
Install AsyncStorage

AsyncStorage is a simple key-value storage system for React Native. It works like localStorage in a web browser, but built specifically for mobile apps.

  • Install the AsyncStorage package by running this command in your terminal:
npx expo install @react-native-async-storage/async-storage
  • Confirm you see a success message in your terminal output.

Seeing an install error?

Make sure your terminal is inside the TodoCalendar project folder. If not, run cd TodoCalendar first and try the install again.

Help me fix my AsyncStorage install error

Add the persistence logic

You need two pieces for persistence: a function to save tasks whenever they change, and a function to load them when the app opens.

  • In app/index.tsx, find the first import line:
import { useState, useCallback } from 'react';
  • Replace it with this updated import that adds useEffect:
import { useState, useEffect, useCallback } from 'react';
  • Now add the AsyncStorage import. Find this line:
import DateTimePicker from '@react-native-community/datetimepicker';
  • Add this new import directly below it:
import AsyncStorage from '@react-native-async-storage/async-storage';

What is AsyncStorage?

AsyncStorage stores data as strings on the device's file system. It persists across app restarts, meaning your tasks will still be there even after you close and reopen the app.

The "async" part means reading and writing happen in the background. This keeps your app responsive while data is being saved.

  • Next, find the line with const [showDatePicker, setShowDatePicker] = useState(false); inside the Index component.
  • Add the following code directly below it:
  useEffect(() => {
    loadTasks();
  }, []);

  useEffect(() => {
    saveTasks(tasks);
  }, [tasks]);

  const loadTasks = async () => {
    try {
      const stored = await AsyncStorage.getItem(STORAGE_KEY);
      if (stored) {
        setTasks(JSON.parse(stored));
      }
    } catch (error) {
      console.error('Failed to load tasks:', error);
    }
  };

  const saveTasks = async (tasksToSave: Task[]) => {
    try {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(tasksToSave));
    } catch (error) {
      console.error('Failed to save tasks:', error);
    }
  };

What does this code do?

  • The first useEffect with an empty [] dependency array runs once when the app opens, calling loadTasks() to restore saved data.
  • The second useEffect with [tasks] runs every time the tasks array changes, automatically saving the latest version to storage.
  • loadTasks reads the stored JSON string and parses it back into an array of task objects.
  • saveTasks converts the tasks array into a JSON string and writes it to the device.
  • Save app/index.tsx.
  • In Expo Go on your phone, add a task (for example, "Buy groceries" with today's date).
  • Close the Expo Go app completely (swipe it away from your recent apps).
  • Reopen Expo Go and load your project again. You should see "Buy groceries" still listed for today.

Tasks disappearing after restart?

Make sure you placed the two useEffect calls inside the Index component (after the state declarations, before the addTask function). If they are outside the component, they will not have access to tasks or setTasks.

Also check that the STORAGE_KEY constant is defined as '@todo_tasks' above the component.

Help me debug why my tasks aren't persisting with AsyncStorage

✔️ Awesome, I've got everything!

Great. Double check you have saved app/index.tsx before moving on.

ⓧ I'd like to double check the full code

import { useState, useEffect, useCallback } from 'react';
import {
  StyleSheet,
  View,
  Text,
  TextInput,
  TouchableOpacity,
  FlatList,
  Platform,
} from 'react-native';
import { Calendar } from 'react-native-calendars';
import DateTimePicker from '@react-native-community/datetimepicker';
import AsyncStorage from '@react-native-async-storage/async-storage';

interface Task {
  id: string;
  title: string;
  date: string;
  completed: boolean;
}

const STORAGE_KEY = '@todo_tasks';

export default function Index() {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [taskTitle, setTaskTitle] = useState('');
  const [selectedDate, setSelectedDate] = useState(
    new Date().toISOString().split('T')[0]
  );
  const [dueDate, setDueDate] = useState(new Date());
  const [showDatePicker, setShowDatePicker] = useState(false);

  useEffect(() => {
    loadTasks();
  }, []);

  useEffect(() => {
    saveTasks(tasks);
  }, [tasks]);

  const loadTasks = async () => {
    try {
      const stored = await AsyncStorage.getItem(STORAGE_KEY);
      if (stored) {
        setTasks(JSON.parse(stored));
      }
    } catch (error) {
      console.error('Failed to load tasks:', error);
    }
  };

  const saveTasks = async (tasksToSave: Task[]) => {
    try {
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(tasksToSave));
    } catch (error) {
      console.error('Failed to save tasks:', error);
    }
  };

  const addTask = () => {
    if (!taskTitle.trim()) return;
    const newTask: Task = {
      id: Date.now().toString(),
      title: taskTitle.trim(),
      date: dueDate.toISOString().split('T')[0],
      completed: false,
    };
    setTasks((prev) => [...prev, newTask]);
    setTaskTitle('');
    setDueDate(new Date());
  };

  const toggleTask = (id: string) => {
    setTasks((prev) =>
      prev.map((task) =>
        task.id === id ? { ...task, completed: !task.completed } : task
      )
    );
  };

  const deleteTask = (id: string) => {
    setTasks((prev) => prev.filter((task) => task.id !== id));
  };

  const onDatePickerChange = (event: any, date?: Date) => {
    setShowDatePicker(Platform.OS === 'ios');
    if (date) {
      setDueDate(date);
    }
  };

  const getMarkedDates = useCallback(() => {
    const marks: Record<string, { marked: boolean; dotColor: string }> = {};
    tasks.forEach((task) => {
      if (!task.completed) {
        marks[task.date] = { marked: true, dotColor: '#4CAF50' };
      }
    });
    marks[selectedDate] = {
      ...marks[selectedDate],
      selected: true,
      selectedColor: '#2196F3',
    } as any;
    return marks;
  }, [tasks, selectedDate]);

  const filteredTasks = tasks.filter((task) => task.date === selectedDate);

  return (
    <View style={styles.container}>
      <Text style={styles.header}>My Tasks</Text>
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          placeholder="Add a new task..."
          value={taskTitle}
          onChangeText={setTaskTitle}
          onSubmitEditing={addTask}
        />
        <TouchableOpacity
          style={styles.dateButton}
          onPress={() => setShowDatePicker(true)}
        >
          <Text style={styles.dateButtonText}>
            {dueDate.toLocaleDateString()}
          </Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.addButton} onPress={addTask}>
          <Text style={styles.addButtonText}>+</Text>
        </TouchableOpacity>
      </View>
      {showDatePicker && (
        <DateTimePicker
          value={dueDate}
          mode="date"
          display="default"
          onChange={onDatePickerChange}
          minimumDate={new Date()}
        />
      )}
      <Calendar
        markedDates={getMarkedDates()}
        onDayPress={(day: { dateString: string }) =>
          setSelectedDate(day.dateString)
        }
        theme={{
          todayTextColor: '#2196F3',
          selectedDayBackgroundColor: '#2196F3',
          dotColor: '#4CAF50',
        }}
      />
      <Text style={styles.dateLabel}>Tasks for {selectedDate}</Text>
      <FlatList
        data={filteredTasks}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.taskItem}>
            <TouchableOpacity
              style={styles.taskContent}
              onPress={() => toggleTask(item.id)}
            >
              <View
                style={[
                  styles.checkbox,
                  item.completed && styles.checkboxChecked,
                ]}
              />
              <Text
                style={[
                  styles.taskText,
                  item.completed && styles.taskTextCompleted,
                ]}
              >
                {item.title}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={() => deleteTask(item.id)}>
              <Text style={styles.deleteButton}>✕</Text>
            </TouchableOpacity>
          </View>
        )}
        ListEmptyComponent={
          <Text style={styles.emptyText}>No tasks for this day</Text>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
    paddingTop: 60,
    paddingHorizontal: 16,
  },
  header: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 16,
    color: '#333',
  },
  inputRow: {
    flexDirection: 'row',
    marginBottom: 16,
    gap: 8,
  },
  input: {
    flex: 1,
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButton: {
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    justifyContent: 'center',
    borderWidth: 1,
    borderColor: '#ddd',
  },
  dateButtonText: {
    fontSize: 12,
    color: '#666',
  },
  addButton: {
    backgroundColor: '#2196F3',
    borderRadius: 8,
    width: 44,
    justifyContent: 'center',
    alignItems: 'center',
  },
  addButtonText: {
    color: '#fff',
    fontSize: 24,
    fontWeight: 'bold',
  },
  dateLabel: {
    fontSize: 16,
    fontWeight: '600',
    marginTop: 16,
    marginBottom: 8,
    color: '#555',
  },
  taskItem: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#fff',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
  },
  taskContent: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  checkbox: {
    width: 22,
    height: 22,
    borderRadius: 11,
    borderWidth: 2,
    borderColor: '#2196F3',
    marginRight: 12,
  },
  checkboxChecked: {
    backgroundColor: '#2196F3',
  },
  taskText: {
    fontSize: 16,
    color: '#333',
  },
  taskTextCompleted: {
    textDecorationLine: 'line-through',
    color: '#999',
  },
  deleteButton: {
    fontSize: 18,
    color: '#ff4444',
    padding: 4,
  },
  emptyText: {
    textAlign: 'center',
    color: '#999',
    marginTop: 20,
    fontSize: 14,
  },
});

Your tasks now persist between sessions. You have a fully functional todo app with a calendar view, task management, and local storage. Next up, the Secret Mission will level up your app with notification reminders.

Secret mission

Add Local Notification Reminders

Your todo app tracks tasks, but it can't remind you about them. In this secret mission, you'll add scheduled local notifications that alert you on your phone when a task is due. You'll request permissions, schedule notifications at creation time, and cancel them when tasks are completed or deleted.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your computer and phone, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The TodoCalendar project folder on your Desktop.
  • The Expo dev server running in your terminal.
  • The Expo Go app installed on your phone.

Keep everything running

No action needed. Choose this if you're still actively building or want to keep using the app on your phone daily.

  • Your Expo dev server stays running in the terminal. You can reconnect from Expo Go anytime.
  • Your tasks are saved in AsyncStorage on your phone. They persist between sessions.
  • Node.js remains installed on your machine for future projects.

Pause - I'll come back to this later

Shut down running processes to free up memory, but keep your project files for later.

  • Switch to the terminal where the Expo dev server is running.
  • Press Ctrl+C to stop the dev server.
  • Your TodoCalendar folder stays on your Desktop. Run npx expo start inside it to resume development anytime.
  • Keep Expo Go installed on your phone so you can reconnect quickly.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Switch to the terminal where the Expo dev server is running.
  • Press Ctrl+C to stop the dev server.
  • Delete the project folder by running:

macOS

cd ~/Desktop
rm -rf TodoCalendar

Windows

cd ~\Desktop
rmdir /s /q TodoCalendar
  • On your phone, uninstall the Expo Go app the same way you would uninstall any app (long-press the icon and select delete or uninstall).

That's a Wrap!

That's a Wrap!

Nice work! You just built your first mobile app using React Native and Expo. You have a real, working todo list app running on your physical phone with a calendar view, persistent storage, and task management.

You've learned how to:

  • Built a complete React Native app from scratch with a task input form and native date picker.
  • Integrated a calendar component that visually marks task days and filters tasks by selected date.
  • Added AsyncStorage for persistent local storage so tasks survive app restarts, plus complete and delete functionality for managing tasks.
  • Secret Mission: Added scheduled local notification reminders that alert you when a task is due.

Ready to quiz yourself?