ProjectCore· 150 min read

Project: Build a Notes App

Combine everything — input, state, FlatList and AsyncStorage — into a real notes app that remembers your notes.

What you will learn

  • Add and list notes with TextInput and FlatList
  • Delete a note with a tap
  • Save notes on the device so they survive a restart

What you will build

A working Notes app: type a note, tap Add, and it appears in a scrollable list. Tap a note to delete it. Best of all, the notes are saved on the phone, so they are still there after you close and reopen the app. You will use every core idea from this subject.

  • TextInput + useState to type a new note.
  • FlatList to show all notes.
  • TouchableOpacity to delete a note on tap.
  • AsyncStorage to save and load notes.

Step 1 — Add and list notes

Start simple: keep notes in state, add on a button tap, and show them with a FlatList.

Step 1: type, add to state, and list with FlatList
import { useState } from 'react';
import { View, TextInput, Button, FlatList, Text, StyleSheet } from 'react-native';

export default function App() {
  const [text, setText] = useState('');
  const [notes, setNotes] = useState([]);

  function addNote() {
    if (text.trim() === '') return;          // ignore empty notes
    const note = { id: Date.now().toString(), text: text };
    setNotes([note, ...notes]);              // newest first
    setText('');                             // clear the box
  }

  return (
    <View style={styles.screen}>
      <TextInput
        style={styles.input}
        placeholder="Write a note..."
        value={text}
        onChangeText={setText}
      />
      <Button title="Add" onPress={addNote} />
      <FlatList
        data={notes}
        keyExtractor={(n) => n.id}
        renderItem={({ item }) => <Text style={styles.note}>{item.text}</Text>}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, padding: 20, paddingTop: 60 },
  input: { borderWidth: 1, borderColor: '#ccc', padding: 10, borderRadius: 6, marginBottom: 10 },
  note: { padding: 14, borderBottomWidth: 1, borderColor: '#eee', fontSize: 16 },
});

Note: Output: Type "Buy milk", tap Add -> it appears in the list and the box clears. Type "Call mom", tap Add -> the list now shows: Call mom Buy milk Using Date.now() as the id gives each note a unique key. Empty notes are ignored.

Step 2 — Delete a note on tap

Add a deleteNote function, then wrap each note in a TouchableOpacity so a tap removes it. The trick is filter: it builds a new list keeping every note except the one you tapped.

Step 2: filter removes the tapped note; TouchableOpacity makes the row tappable
import { TouchableOpacity } from 'react-native';

// add this function inside App, next to addNote:
function deleteNote(id) {
  setNotes(notes.filter((n) => n.id !== id));   // keep all but this id
}

// and change renderItem to make each row tappable:
<FlatList
  data={notes}
  keyExtractor={(n) => n.id}
  renderItem={({ item }) => (
    <TouchableOpacity onPress={() => deleteNote(item.id)}>
      <Text style={styles.note}>{item.text}</Text>
    </TouchableOpacity>
  )}
/>

Note: Output: Tap "Buy milk" and it disappears from the list, leaving: Call mom filter keeps every note whose id is not the one tapped, so only that note is removed. Because notes changed via setNotes, the FlatList redraws by itself.

Step 3 — Save notes on the device

Now make notes survive a restart. Load them once when the app opens, and save whenever they change, using AsyncStorage with JSON.

Step 3: load on open, save on every change
import { useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

// load saved notes once, when the app opens
useEffect(() => {
  AsyncStorage.getItem('notes').then((raw) => {
    if (raw) setNotes(JSON.parse(raw));
  });
}, []);

// save whenever notes change
useEffect(() => {
  AsyncStorage.setItem('notes', JSON.stringify(notes));
}, [notes]);

Note: Output: Add a few notes, fully close the app, and reopen it -> your notes are still there. The first effect runs once to load. The second runs whenever notes changes (note the [notes]) and saves the latest list.

Tip: Order matters here. The load effect has [] so it runs only once, on open. The save effect has [notes] so it runs every time the list changes — including right after loading, which simply re-saves the same notes. That is harmless, so you do not need to worry about the two effects clashing.

Your tasks

  1. Get Step 1 working: add notes and see them in the list.
  2. Add Step 2 so tapping a note deletes it.
  3. Add Step 3 so notes are saved and reload after a restart.
  4. Polish it: style the input and notes, and add a heading like "My Notes".

Stretch goals

  • Show a "No notes yet" message with ListEmptyComponent when the list is empty.
  • Add a confirm step before deleting, using Alert.alert with two buttons.
  • Add a second screen (a stack) that shows one note in full when tapped.

Watch out: Build in small steps and test after each one. Get Step 1 fully working before adding delete, and add saving last. A small finished app beats a big broken one.

Note: When this works you have built a real, useful mobile app that remembers data on the device — using input, state, lists and storage together. Run it on your own phone and add it to your portfolio!

Q. In this app, which piece makes the notes survive after you fully close and reopen it?

Answer: useState is forgotten when the app closes. Saving to and loading from AsyncStorage is what keeps the notes between launches.

✍️ Practice

  1. Add a "Clear all" button that empties the notes and the saved storage.
  2. Show the total number of notes in the heading, e.g. "My Notes (3)".

🏠 Homework

  1. Extend the app or rebuild it as a simple weather or expense tracker, reusing the same input + FlatList + AsyncStorage pattern. Write a short note on what you changed.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →