Build a Habit Tracker with React Native

Build a persistent habit tracker with React Native and Expo.

Introduction

30 Second Summary

Small habits are easy to forget once the day gets busy. A simple place to record each habit makes progress easier to see.

In this project, you will build Pocket Habit Tracker with React Native and Expo. You will create habits, mark them complete, track your totals, delete old habits, and keep the list after a reload.

What You'll Build

Picture opening a phone-shaped habit dashboard where your routines return after a refresh with their progress intact.

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

  • A controlled text input where you can name a habit and add it to your list.
  • Interactive Todo and Done controls for changing each habit's status. You can also delete habits while the total and completed counts update immediately.
  • Persistent Async Storage that restores your habit names and completion states after a browser reload.
  • Secret Mission: Add All, Active, and Done filters without changing the stored habit data.

Are there any prerequisites?

This project assumes Visual Studio Code and Node.js are already installed on your Windows computer. You also need internet access for package installation.

No prior programming experience is required.

Before We Start

Before the hands-on work begins, this step helps you commit to building Pocket Habit Tracker as your first small app. It gives every new idea about React Native interfaces, state, interactions, and local persistence a clear purpose.

Set Up the Expo Project

Your habit tracker needs a reliable place to run before you build its interface. A verified Node.js runtime lets Expo serve the React Native starter without toolchain problems hiding the mobile concepts.

In this step, you will verify Node.js plus npm. Then you will scaffold a blank Expo project that runs in your browser.

In this step, get ready to:
  • Verify that Node.js meets Expo's minimum version requirement.
  • Scaffold a blank Expo project called pocket-habit-tracker.
  • Install browser support and launch the Expo starter app.
Verify Node.js and npm

Expo SDK 57 requires Node.js 22.13.x or newer. You will run both version checks inside the integrated PowerShell terminal in Visual Studio Code.

  • Click View in the Visual Studio Code menu bar.
  • Select Terminal to display the integrated terminal.
  • Select PowerShell from the terminal dropdown if another shell is active.
  • Check the installed Node.js version plus the npm version by running:
node -v
npm -v

What do these checks confirm?

  • The node command reports the installed Node.js version.
  • The npm command confirms that the package manager is available.

✔️ I see version 22.13.x or newer

That clears the first setup hurdle. Node.js 22.13.x or newer can run the Expo SDK 57 toolchain.

  • Keep the integrated PowerShell terminal open for the project commands.

ⓧ I see an older version

Your existing Node.js installation is below Expo's minimum requirement. The current Windows LTS release is v24.21.0 LTS.

  • Open the official Node.js download page.
  • Download the Windows installer for v24.21.0 LTS.
  • Run the downloaded installer.
  • Complete the installation using the default options.

Restarting Visual Studio Code lets its integrated terminal detect the updated Node.js installation.

  • Close Visual Studio Code.
  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to reopen Visual Studio Code.
  • Click View in the menu bar.
  • Select Terminal to display the integrated terminal.
  • Repeat both version checks by running:
node -v
npm -v

What should the checks show?

The first line should now report Node.js v24.21.0 or another supported version. The second line should report an npm version.

ⓧ Command not found

PowerShell cannot find Node.js on your computer. Installing the current LTS release provides both Node.js plus npm.

  • Open the official Node.js download page.
  • Download the Windows installer for v24.21.0 LTS.
  • Run the downloaded installer.
  • Complete the installation using the default options.

A fresh Visual Studio Code session picks up the commands added by the installer.

  • Close Visual Studio Code.
  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to reopen Visual Studio Code.
  • Click View in the menu bar.
  • Select Terminal to display the integrated terminal.
  • Repeat both version checks by running:
node -v
npm -v

What should the checks show?

The first line should report Node.js v24.21.0 or another supported version. The second line should report an npm version.

Scaffold the blank project

The blank template contains the minimum files needed for a one-screen Expo app. This keeps navigation code outside your first React Native project.

Why use the blank template?

Expo's default templates can include navigation structure for larger apps. Pocket Habit Tracker uses one screen, so the blank template keeps every file focused on the interface you are about to build.

  • Move the PowerShell terminal to your Desktop by running this command:
cd ~/Desktop

What does this command do?

PowerShell treats ~ as your home folder. This command makes the Desktop the location where the project folder is created.

The first scaffold can take a few minutes while npm downloads the template packages. A pause in the terminal during this download is expected.

  • Approve the temporary scaffolder download if npm asks for permission.
  • Start the blank Expo scaffolder by running:
npx create-expo-app@latest --template blank

What does this command create?

The Expo scaffolder creates a project using the minimum blank template. The generated dependencies include Expo SDK 57, React 19.2.3, plus React Native 0.86.3.

  • Enter pocket-habit-tracker when PowerShell asks for the project name.
  • Press Enter to confirm the name.
  • Wait for PowerShell to return to its prompt after the dependencies finish installing.

The completed scaffold creates a pocket-habit-tracker folder on your Desktop.

  • Move into the generated pocket-habit-tracker folder by running:
cd pocket-habit-tracker

Why change folders?

Expo commands read the project configuration from the current folder. Moving into pocket-habit-tracker ensures the next commands use this app.

  • Confirm that the scaffolded files exist by running:
ls

What does this command show?

The ls command lists the contents of the current project folder.

You should see App.js in the output. You should also see package.json.

That is the project foundation in place. The untouched blank starter is ready for browser support.

Project folder missing?

  • Check that you entered pocket-habit-tracker as the project name.
  • Return to your Desktop before repeating the scaffold if PowerShell reports that the folder cannot be found.
  • Ask for help with the scaffold error using help me troubleshoot my blank Expo project setup.
Install web support and launch Expo

A browser preview needs React Native for Web to translate React Native components for the browser. Expo also needs its web runtime plus React's browser renderer.

  • Install the browser support packages inside pocket-habit-tracker by running:
npx expo install react-dom react-native-web @expo/metro-runtime

What does this installation add?

  • Expo selects package versions compatible with SDK 57.
  • The react-dom package renders React in the browser.
  • The react-native-web package provides web implementations of React Native components.
  • The @expo/metro-runtime package supports Expo's web development runtime.
  • Wait for PowerShell to return to the project prompt.

The returned prompt confirms that the web packages were added to package.json without an installation error.

Web packages not installing?

  • Check that the terminal path ends with pocket-habit-tracker.
  • Check your internet connection before repeating the installation.
  • Ask for help using help me fix my Expo web dependency installation.

The development command keeps this PowerShell terminal occupied while Expo serves the app. The terminal remains occupied until you stop the server with Ctrl+C.

Before you start it, what do you expect the browser to show from a blank Expo project?

  • Start the Expo development server in browser mode by running:
npx expo start --web

What does this command do?

Expo starts its local development server in web mode. It opens the browser preview for the blank starter app.

You should see the Expo starter message in your browser. The PowerShell terminal stays active because it is serving the preview.

You have your first visible mobile app result. The blank React Native starter now runs through Expo in your browser.

Browser preview not loading?

  • Check the PowerShell terminal for an installation or network error.
  • Confirm that the terminal path ends with pocket-habit-tracker.
  • Ask for help using help me troubleshoot my Expo browser preview.

✔️ Awesome, I've got everything!

Your pocket-habit-tracker project is ready. The Expo development server is running with its starter app visible in your browser.

ⓧ I'd like to double check the full code

This step uses Expo's generated blank starter, so you have not edited application code yet. Compare your project with these checkpoints.

  • The pocket-habit-tracker folder was created with the blank Expo scaffolder. It is configured for Expo SDK 57 plus browser preview.
  • The untouched blank Expo starter App.js file is present. It runs in the browser.
  • The package.json manifest includes react-dom, react-native-web, plus @expo/metro-runtime for web support.

Your Expo starter now runs from the browser-ready project. Next up, you will replace it with the first working version of Pocket Habit Tracker.

Build the First Working Habit List

Your Expo starter already runs in the browser. That working preview gives you a place to turn React Native components into a useful app.

An in-memory habit list creates your first complete interaction with state. A deliberate reload then reveals why temporary data cannot support a lasting habit tracker.

In this step, get ready to:
  • Define the habit state with an immutable add action.
  • Build a styled input for new habits.
  • Render a habit list that reveals its temporary state after a reload.
Add the habit state and update logic

The app needs one value for the text being typed. It also needs an array for every habit added during the current session.

  • In the VS Code Explorer sidebar, select App.js.
  • Press Ctrl+A to select the starter code.
  • Replace the selected starter code with this first section:
import { useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';

export default function App() {
  const [habitName, setHabitName] = useState('');
  const [habits, setHabits] = useState([]);

  function addHabit() {
    const cleanName = habitName.trim();

    if (!cleanName) {
      return;
    }

    const newHabit = {
      id: `${Date.now()}-${Math.random()}`,
      name: cleanName,
      completed: false,
    };

    setHabits((currentHabits) => [...currentHabits, newHabit]);
    setHabitName('');
  }

How Does the State Logic Work?

  • The habitName value holds the current input text.
  • The habits array holds the habits added during this session.
  • The trim() call removes whitespace from the start or end of a name.
  • The newHabit object receives a unique identifier from Date.now() plus Math.random().
  • The spread syntax creates a new array containing the existing habits plus the new habit.

This is the first section of the component. The next block completes it before you save the file.

  • Paste this second section directly below the closing brace of addHabit():
  return (
    <View style={styles.screen}>
      <View style={styles.card}>
        <Text style={styles.eyebrow}>DAILY ROUTINE</Text>
        <Text style={styles.title}>Pocket Habit Tracker</Text>
        <Text style={styles.subtitle}>
          Add one small action, then check it off when it is done.
        </Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, backgroundColor: '#e8eef8', paddingHorizontal: 20, paddingTop: 48 },
  card: { width: '100%', maxWidth: 560, alignSelf: 'center', backgroundColor: '#ffffff', borderRadius: 24, padding: 24 },
  eyebrow: { color: '#4f46e5', fontSize: 12, fontWeight: '700', letterSpacing: 1.4 },
  title: { color: '#172033', fontSize: 30, fontWeight: '800', marginTop: 6 },
  subtitle: { color: '#64748b', fontSize: 16, lineHeight: 23, marginTop: 8 },
});

What Does This Interface Render?

  • The JSX returns a screen containing one centered card.
  • The text components establish the tracker name plus its purpose.
  • The StyleSheet values create the blue background plus the white card.
  • Press Ctrl+S to save App.js.
  • Switch back to the Expo browser preview from earlier.

You should see a white card with the Pocket Habit Tracker title on a pale blue background. Your starter now has the visual identity of the app you are building.

Still Seeing the Starter App?

  • Confirm that you edited App.js inside the pocket-habit-tracker folder.
  • Confirm that the file is saved in VS Code.
  • Check the running PowerShell terminal for a syntax problem near the line reported.
  • Help me find why my Expo browser preview is not showing the updated App.js file.
Build the controlled input

A controlled text input displays a value from React state. Every text change updates that same value through setHabitName.

  • In App.js, find the closing </Text> line under the subtitle.
  • Place your cursor on the blank line below that closing tag.
  • Add the input row by pasting this block:
        <View style={styles.inputRow}>
          <TextInput
            accessibilityLabel="New habit name"
            onChangeText={setHabitName}
            onSubmitEditing={addHabit}
            placeholder="Example: Drink water"
            returnKeyType="done"
            style={styles.input}
            value={habitName}
          />
          <Pressable
            accessibilityLabel="Add habit"
            accessibilityRole="button"
            onPress={addHabit}
            style={({ pressed }) => [styles.addButton, pressed && styles.pressed]}
          >
            <Text style={styles.addButtonText}>Add</Text>
          </Pressable>
        </View>

How Does the Input Stay in Sync?

  • The value prop displays the current habitName value.
  • The onChangeText prop sends each text change to setHabitName.
  • The Add control calls addHabit through its onPress handler.
  • Submitting the input also calls addHabit through onSubmitEditing.
  • Scroll to the StyleSheet.create block at the bottom of App.js.
  • Place your cursor before the final }); line.
  • Add the input styles by pasting this block:
  inputRow: { flexDirection: 'row', marginTop: 24 },
  input: { flex: 1, borderColor: '#cbd5e1', borderWidth: 1, borderRadius: 12, color: '#172033', fontSize: 16, paddingHorizontal: 14, paddingVertical: 12 },
  addButton: { justifyContent: 'center', backgroundColor: '#4f46e5', borderRadius: 12, marginLeft: 10, paddingHorizontal: 20 },
  addButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '700' },
  pressed: { opacity: 0.7 },

What Do These Styles Change?

  • The inputRow style places the input beside the Add control.
  • The input style gives the text field its border plus spacing.
  • The addButton style creates the purple action control.
  • The pressed style lowers the opacity while the control is being pressed.
  • Press Ctrl+S to save App.js.
  • Switch back to the running browser preview.
  • Type Drink water into the new text field.
  • Press the Add control.

The text field should clear after you press Add. That visible reset proves the input state reaches addHabit().

Input Not Clearing?

  • Check that value is set to habitName.
  • Check that onChangeText uses setHabitName.
  • Check that the Add control uses onPress={addHabit}.
  • Help me debug why my controlled habit input does not clear after I press Add.
Render habits and test a reload

The habit array now changes when you add an item. Rendering that array turns the hidden state update into one visible row per habit.

  • In App.js, find the closing </View> tag for styles.inputRow.
  • Place your cursor on the blank line below that tag.
  • Add the empty state plus rendered rows by pasting this block:
        {habits.length === 0 ? (
          <Text style={styles.emptyText}>No habits yet. Add your first one.</Text>
        ) : (
          <View style={styles.list}>
            {habits.map((habit) => (
              <View key={habit.id} style={styles.habitRow}>
                <Text style={styles.statusBadge}>Todo</Text>
                <Text style={styles.habitName}>{habit.name}</Text>
              </View>
            ))}
          </View>
        )}

How Does the List Choose What to Show?

  • The length check displays the empty message when habits contains no items.
  • The map() call creates one row for each stored habit.
  • The habit.id value gives each rendered row a stable key.
  • The habit.name value places the typed name inside its row.
  • Scroll to the bottom of the StyleSheet.create block.
  • Place your cursor before the final }); line.
  • Add the list styles by pasting this block:
  list: { marginTop: 18 },
  habitRow: { flexDirection: 'row', borderColor: '#e2e8f0', borderTopWidth: 1, paddingVertical: 12 },
  statusBadge: { width: 48, color: '#475569', fontSize: 12, fontWeight: '700' },
  habitName: { flex: 1, color: '#172033', fontSize: 16 },
  emptyText: { color: '#64748b', fontSize: 15, marginTop: 24, textAlign: 'center' },

What Do the List Styles Add?

  • The habitRow style places the status beside the habit name.
  • The top border separates each row from the one above it.
  • The statusBadge style reserves consistent space for the Todo label.
  • The emptyText style centers the message shown before any habits exist.
  • Press Ctrl+S to save App.js.
  • Switch back to the running browser preview.

You should see the empty message beneath the input. The app now has a clear starting state.

List Area Missing?

  • Confirm that the conditional block sits below the input row inside the white card.
  • Confirm that every opening View has a matching closing tag.
  • Check that the list uses habits.map with the singular name habit.
  • Help me debug why my React Native habit list or empty state is not rendering.

Before you test it, picture what the list should show after two presses of Add.

  • Type Drink water into the habit field.
  • Press the Add control.
  • Type Stretch into the habit field.
  • Press the Add control.

You should see two separate rows labeled Todo. That is your first working habit loop: type a name, add it, then see it rendered from state.

Before you reload, predict whether those two rows will survive.

  • Reload the browser preview.

You should see the empty message again. This reset is intentional because the habits array currently lives only in memory.

✔️ Awesome, I've got everything!

Your App.js file is saved. You can add habits during the running session and reproduce the empty state with a reload.

ⓧ I'd like to double check the full code

Compare your App.js file with the complete version for this step.

import { useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';

export default function App() {
  const [habitName, setHabitName] = useState('');
  const [habits, setHabits] = useState([]);

  function addHabit() {
    const cleanName = habitName.trim();

    if (!cleanName) {
      return;
    }

    const newHabit = {
      id: `${Date.now()}-${Math.random()}`,
      name: cleanName,
      completed: false,
    };

    setHabits((currentHabits) => [...currentHabits, newHabit]);
    setHabitName('');
  }

  return (
    <View style={styles.screen}>
      <View style={styles.card}>
        <Text style={styles.eyebrow}>DAILY ROUTINE</Text>
        <Text style={styles.title}>Pocket Habit Tracker</Text>
        <Text style={styles.subtitle}>
          Add one small action, then check it off when it is done.
        </Text>

        <View style={styles.inputRow}>
          <TextInput
            accessibilityLabel="New habit name"
            onChangeText={setHabitName}
            onSubmitEditing={addHabit}
            placeholder="Example: Drink water"
            returnKeyType="done"
            style={styles.input}
            value={habitName}
          />
          <Pressable
            accessibilityLabel="Add habit"
            accessibilityRole="button"
            onPress={addHabit}
            style={({ pressed }) => [styles.addButton, pressed && styles.pressed]}
          >
            <Text style={styles.addButtonText}>Add</Text>
          </Pressable>
        </View>

        {habits.length === 0 ? (
          <Text style={styles.emptyText}>No habits yet. Add your first one.</Text>
        ) : (
          <View style={styles.list}>
            {habits.map((habit) => (
              <View key={habit.id} style={styles.habitRow}>
                <Text style={styles.statusBadge}>Todo</Text>
                <Text style={styles.habitName}>{habit.name}</Text>
              </View>
            ))}
          </View>
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, backgroundColor: '#e8eef8', paddingHorizontal: 20, paddingTop: 48 },
  card: { width: '100%', maxWidth: 560, alignSelf: 'center', backgroundColor: '#ffffff', borderRadius: 24, padding: 24 },
  eyebrow: { color: '#4f46e5', fontSize: 12, fontWeight: '700', letterSpacing: 1.4 },
  title: { color: '#172033', fontSize: 30, fontWeight: '800', marginTop: 6 },
  subtitle: { color: '#64748b', fontSize: 16, lineHeight: 23, marginTop: 8 },
  inputRow: { flexDirection: 'row', marginTop: 24 },
  input: { flex: 1, borderColor: '#cbd5e1', borderWidth: 1, borderRadius: 12, color: '#172033', fontSize: 16, paddingHorizontal: 14, paddingVertical: 12 },
  addButton: { justifyContent: 'center', backgroundColor: '#4f46e5', borderRadius: 12, marginLeft: 10, paddingHorizontal: 20 },
  addButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '700' },
  pressed: { opacity: 0.7 },
  list: { marginTop: 18 },
  habitRow: { flexDirection: 'row', borderColor: '#e2e8f0', borderTopWidth: 1, paddingVertical: 12 },
  statusBadge: { width: 48, color: '#475569', fontSize: 12, fontWeight: '700' },
  habitName: { flex: 1, color: '#172033', fontSize: 16 },
  emptyText: { color: '#64748b', fontSize: 15, marginTop: 24, textAlign: 'center' },
});

What Should the Full File Contain?

The file combines two state values with the addHabit() handler. Its interface connects that logic to the controlled input plus the rendered habit rows.

Your first habit list now works for the current session. Next, you will turn each row into an interactive control with completion, deletion, and live progress counts.

Complete, Count, and Delete Habits

Your React Native habit list can already collect entries during a browser session. Those static rows now need controls that turn the list into a useful tracker.

In this step, you use state to complete habits or remove them. You also calculate live progress from the current list.

In this step, get ready to:
  • Toggle a selected habit between Todo and Done.
  • Delete individual habits with accessible controls.
  • Display live total and completed counts.
Make habits toggle between Todo and Done

An immutable array update creates a new array whenever a habit changes. The map() method can copy every habit while replacing only the selected one.

  • In App.js find the closing brace of addHabit().
  • Add the completion handler directly below addHabit() by pasting this code:
  function toggleHabit(id) {
    setHabits((currentHabits) =>
      currentHabits.map((habit) =>
        habit.id === id
          ? { ...habit, completed: !habit.completed }
          : habit,
      ),
    );
  }

How Does Toggling Work?

  • The id parameter identifies the selected habit.
  • The map() call builds a new array with the same number of habits.
  • The matching habit becomes a copied object with its completed value reversed.
  • Every other habit remains unchanged.
  • Save App.js.

Good progress. Your browser preview refreshes without a red error screen because the new handler compiles successfully.

Seeing an Error After Saving?

  • Check that toggleHabit() sits inside App().
  • Compare the closing parentheses around map() with the snippet above.
  • Ask for help with the handler syntax.

The handler needs an event handler in each row before you can trigger it. A checkbox-style Pressable can update the habit while exposing its checked state to accessibility tools.

  • Find the two Text elements inside habits.map().
  • Replace both elements with this completion control:
                <Pressable
                  accessibilityLabel={`Mark ${habit.name} as ${
                    habit.completed ? 'active' : 'completed'
                  }`}
                  accessibilityRole="checkbox"
                  accessibilityState={{ checked: habit.completed }}
                  onPress={() => toggleHabit(habit.id)}
                  style={({ pressed }) => [
                    styles.habitMain,
                    pressed && styles.pressed,
                  ]}
                >
                  <Text
                    style={[
                      styles.statusBadge,
                      habit.completed && styles.statusBadgeDone,
                    ]}
                  >
                    {habit.completed ? 'Done' : 'Todo'}
                  </Text>
                  <Text
                    style={[
                      styles.habitName,
                      habit.completed && styles.habitNameDone,
                    ]}
                  >
                    {habit.name}
                  </Text>
                </Pressable>

What Does This Control Do?

  • The onPress handler passes the selected habit's id to toggleHabit().
  • The accessibility properties describe the control as a checkbox with a current checked state.
  • The conditional text displays Todo for an active habit or Done for a completed habit.
  • The pressed style gives immediate feedback while you hold the control.
  • Save App.js.
  • Add a habit through the running browser preview.
  • Press the Todo area in its row.

You should see Todo change to Done. Press the row again to see it return to Todo.

Does the Status Stay on Todo?

  • Check that onPress calls toggleHabit(habit.id).
  • Confirm that the row still sits inside habits.map() so habit is available.
  • Ask for help tracing the completion control.

The state now changes correctly. The next styles make the whole habit area easy to press while giving completed habits a clear visual treatment.

  • In styles select the declarations from habitRow through statusBadge.
  • Replace the selection with this style block:
  habitRow: {
    flexDirection: 'row',
    alignItems: 'center',
    borderColor: '#e2e8f0',
    borderTopWidth: 1,
    paddingVertical: 12,
  },
  habitMain: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  statusBadge: {
    width: 48,
    color: '#475569',
    fontSize: 12,
    fontWeight: '700',
  },

How Does the Row Layout Change?

  • The habitRow style centers every control along the row.
  • The habitMain style lets the completion control fill the available space.
  • The fixed badge width keeps every habit name aligned.
  • Select the existing habitName declaration.
  • Replace that declaration with these status styles:
  statusBadgeDone: {
    color: '#15803d',
  },
  habitName: {
    flex: 1,
    color: '#172033',
    fontSize: 16,
  },
  habitNameDone: {
    color: '#94a3b8',
    textDecorationLine: 'line-through',
  },

How Do Completed Habits Look?

  • The statusBadgeDone style turns the Done label green.
  • The habitNameDone style fades the habit name.
  • The line through the name makes completion visible without removing the habit.
  • Save App.js.
  • Press the habit row in the browser preview.

You should see the Done label turn green. The habit name should become grey with a line through it.

Missing the Completed Styling?

  • Check that statusBadgeDone matches the name used in the status text style array.
  • Check that habitNameDone matches the name used in the habit text style array.
  • Ask for help with the conditional styles.
Add a delete control

Deleting a habit also uses an immutable array update. The filter() method creates a new array containing every habit except the selected one.

  • Find the closing brace of toggleHabit().
  • Add the deletion handler directly below it by pasting this code:
  function deleteHabit(id) {
    setHabits((currentHabits) =>
      currentHabits.filter((habit) => habit.id !== id),
    );
  }

How Does Deletion Work?

  • The filter() call checks every habit in the current array.
  • A habit remains when its id differs from the selected id.
  • The selected habit fails that check and stays out of the new array.
  • Find the closing Pressable tag for the completion control inside each habit row.
  • Add the delete control immediately below that closing tag:
                <Pressable
                  accessibilityLabel={`Delete ${habit.name}`}
                  accessibilityRole="button"
                  onPress={() => deleteHabit(habit.id)}
                  style={({ pressed }) => [
                    styles.deleteButton,
                    pressed && styles.pressed,
                  ]}
                >
                  <Text style={styles.deleteButtonText}>Delete</Text>
                </Pressable>

What Does the Delete Control Do?

  • The accessibility label includes the habit name so each delete control has a specific purpose.
  • The onPress handler passes the selected habit.id to deleteHabit().
  • The existing pressed style lowers the button opacity during interaction.
  • Save App.js.
  • Add a habit in the browser preview.
  • Press Delete beside that habit.

The selected row should disappear immediately. Any other habits should remain in the list.

  • Add the delete button styles below habitNameDone by pasting this block:
  deleteButton: {
    backgroundColor: '#fee2e2',
    borderRadius: 10,
    marginLeft: 12,
    paddingHorizontal: 12,
    paddingVertical: 8,
  },
  deleteButtonText: {
    color: '#b91c1c',
    fontSize: 13,
    fontWeight: '700',
  },

Why Style Delete Differently?

The pale red background separates deletion from completion. The darker red label makes the destructive action easy to recognize before you press it.

  • Save App.js.
  • Add another habit in the browser preview.

You should see a red Delete button at the end of the new row. The completion area should still fill the rest of the row.

Does Delete Remove the Wrong Habit?

  • Confirm that the delete control passes habit.id to deleteHabit().
  • Check that the filter comparison uses habit.id !== id.
  • Ask for help tracing the selected identifier.
Show live progress counts

The current habits array already contains everything needed for progress totals. A derived value calculates the completed count each time state changes.

  • Find the closing brace of deleteHabit().
  • Add the completed count directly below it:
  const completedCount = habits.filter((habit) => habit.completed).length;

What Does the Count Measure?

The filter keeps only habits whose completed value is true. The resulting array's length becomes the current completed total.

  • Find the closing tag for styles.inputRow in the returned interface.
  • Add the summary cards immediately below that closing tag:
        <View style={styles.summaryRow}>
          <View style={styles.summaryBox}>
            <Text style={styles.summaryNumber}>{habits.length}</Text>
            <Text style={styles.summaryLabel}>Total</Text>
          </View>
          <View style={styles.summaryBox}>
            <Text style={styles.summaryNumber}>{completedCount}</Text>
            <Text style={styles.summaryLabel}>Completed</Text>
          </View>
        </View>

How Do the Totals Stay Current?

  • The Total card reads habits.length from the current array.
  • The Completed card displays the derived completedCount value.
  • Every add, toggle, or delete update causes the component to calculate both values again.
  • Save App.js.

You should see Total and Completed above the habit list. Their numbers respond immediately when the array changes.

  • Add the summary styles below pressed by pasting this block:
  summaryRow: {
    flexDirection: 'row',
    marginTop: 20,
  },
  summaryBox: {
    flex: 1,
    backgroundColor: '#f1f5f9',
    borderRadius: 14,
    padding: 14,
    marginRight: 10,
  },
  summaryNumber: {
    color: '#172033',
    fontSize: 24,
    fontWeight: '800',
  },
  summaryLabel: {
    color: '#64748b',
    fontSize: 13,
    marginTop: 2,
  },

How Are the Summary Cards Arranged?

  • The summary row places both cards on one horizontal line.
  • Each summary box takes an equal share of the available width.
  • The larger number makes progress readable at a glance.
  • Save App.js.

Before you test the finished controls, what totals do you expect after adding three habits, completing one, and deleting another?

  • Add three habits in the running browser preview.
  • Press the Todo area on one habit.
  • Press Delete on a different habit.

You should see 2 under Total and 1 under Completed. The completed row should show Done while the deleted row should be gone.

Are the Counts Incorrect?

  • Confirm that Total displays habits.length.
  • Confirm that completedCount filters habits by habit.completed.
  • Ask for help checking the derived totals.

✔️ Awesome, I've got everything!

Your App.js file now supports adding, completing, counting, and deleting habits during the running session.

ⓧ I'd like to double check the full code

Compare your App.js file with this complete in-memory version.

import { useState } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';

export default function App() {
  const [habitName, setHabitName] = useState('');
  const [habits, setHabits] = useState([]);

  function addHabit() {
    const cleanName = habitName.trim();

    if (!cleanName) {
      return;
    }

    const newHabit = {
      id: `${Date.now()}-${Math.random()}`,
      name: cleanName,
      completed: false,
    };

    setHabits((currentHabits) => [...currentHabits, newHabit]);
    setHabitName('');
  }

  function toggleHabit(id) {
    setHabits((currentHabits) =>
      currentHabits.map((habit) =>
        habit.id === id
          ? { ...habit, completed: !habit.completed }
          : habit,
      ),
    );
  }

  function deleteHabit(id) {
    setHabits((currentHabits) =>
      currentHabits.filter((habit) => habit.id !== id),
    );
  }

  const completedCount = habits.filter((habit) => habit.completed).length;

  return (
    <View style={styles.screen}>
      <View style={styles.card}>
        <Text style={styles.eyebrow}>DAILY ROUTINE</Text>
        <Text style={styles.title}>Pocket Habit Tracker</Text>
        <Text style={styles.subtitle}>
          Add one small action, then check it off when it is done.
        </Text>

        <View style={styles.inputRow}>
          <TextInput
            accessibilityLabel="New habit name"
            onChangeText={setHabitName}
            onSubmitEditing={addHabit}
            placeholder="Example: Drink water"
            returnKeyType="done"
            style={styles.input}
            value={habitName}
          />
          <Pressable
            accessibilityLabel="Add habit"
            accessibilityRole="button"
            onPress={addHabit}
            style={({ pressed }) => [
              styles.addButton,
              pressed && styles.pressed,
            ]}
          >
            <Text style={styles.addButtonText}>Add</Text>
          </Pressable>
        </View>

        <View style={styles.summaryRow}>
          <View style={styles.summaryBox}>
            <Text style={styles.summaryNumber}>{habits.length}</Text>
            <Text style={styles.summaryLabel}>Total</Text>
          </View>
          <View style={styles.summaryBox}>
            <Text style={styles.summaryNumber}>{completedCount}</Text>
            <Text style={styles.summaryLabel}>Completed</Text>
          </View>
        </View>

        {habits.length === 0 ? (
          <Text style={styles.emptyText}>No habits yet. Add your first one.</Text>
        ) : (
          <View style={styles.list}>
            {habits.map((habit) => (
              <View key={habit.id} style={styles.habitRow}>
                <Pressable
                  accessibilityLabel={`Mark ${habit.name} as ${
                    habit.completed ? 'active' : 'completed'
                  }`}
                  accessibilityRole="checkbox"
                  accessibilityState={{ checked: habit.completed }}
                  onPress={() => toggleHabit(habit.id)}
                  style={({ pressed }) => [
                    styles.habitMain,
                    pressed && styles.pressed,
                  ]}
                >
                  <Text
                    style={[
                      styles.statusBadge,
                      habit.completed && styles.statusBadgeDone,
                    ]}
                  >
                    {habit.completed ? 'Done' : 'Todo'}
                  </Text>
                  <Text
                    style={[
                      styles.habitName,
                      habit.completed && styles.habitNameDone,
                    ]}
                  >
                    {habit.name}
                  </Text>
                </Pressable>

                <Pressable
                  accessibilityLabel={`Delete ${habit.name}`}
                  accessibilityRole="button"
                  onPress={() => deleteHabit(habit.id)}
                  style={({ pressed }) => [
                    styles.deleteButton,
                    pressed && styles.pressed,
                  ]}
                >
                  <Text style={styles.deleteButtonText}>Delete</Text>
                </Pressable>
              </View>
            ))}
          </View>
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: {
    flex: 1,
    backgroundColor: '#e8eef8',
    paddingHorizontal: 20,
    paddingTop: 48,
  },
  card: {
    width: '100%',
    maxWidth: 560,
    alignSelf: 'center',
    backgroundColor: '#ffffff',
    borderRadius: 24,
    padding: 24,
  },
  eyebrow: {
    color: '#4f46e5',
    fontSize: 12,
    fontWeight: '700',
    letterSpacing: 1.4,
  },
  title: {
    color: '#172033',
    fontSize: 30,
    fontWeight: '800',
    marginTop: 6,
  },
  subtitle: {
    color: '#64748b',
    fontSize: 16,
    lineHeight: 23,
    marginTop: 8,
  },
  inputRow: {
    flexDirection: 'row',
    marginTop: 24,
  },
  input: {
    flex: 1,
    borderColor: '#cbd5e1',
    borderWidth: 1,
    borderRadius: 12,
    color: '#172033',
    fontSize: 16,
    paddingHorizontal: 14,
    paddingVertical: 12,
  },
  addButton: {
    justifyContent: 'center',
    backgroundColor: '#4f46e5',
    borderRadius: 12,
    marginLeft: 10,
    paddingHorizontal: 20,
  },
  addButtonText: {
    color: '#ffffff',
    fontSize: 16,
    fontWeight: '700',
  },
  pressed: {
    opacity: 0.7,
  },
  summaryRow: {
    flexDirection: 'row',
    marginTop: 20,
  },
  summaryBox: {
    flex: 1,
    backgroundColor: '#f1f5f9',
    borderRadius: 14,
    padding: 14,
    marginRight: 10,
  },
  summaryNumber: {
    color: '#172033',
    fontSize: 24,
    fontWeight: '800',
  },
  summaryLabel: {
    color: '#64748b',
    fontSize: 13,
    marginTop: 2,
  },
  list: {
    marginTop: 18,
  },
  habitRow: {
    flexDirection: 'row',
    alignItems: 'center',
    borderColor: '#e2e8f0',
    borderTopWidth: 1,
    paddingVertical: 12,
  },
  habitMain: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  statusBadge: {
    width: 48,
    color: '#475569',
    fontSize: 12,
    fontWeight: '700',
  },
  statusBadgeDone: {
    color: '#15803d',
  },
  habitName: {
    flex: 1,
    color: '#172033',
    fontSize: 16,
  },
  habitNameDone: {
    color: '#94a3b8',
    textDecorationLine: 'line-through',
  },
  deleteButton: {
    backgroundColor: '#fee2e2',
    borderRadius: 10,
    marginLeft: 12,
    paddingHorizontal: 12,
    paddingVertical: 8,
  },
  deleteButtonText: {
    color: '#b91c1c',
    fontSize: 13,
    fontWeight: '700',
  },
  emptyText: {
    color: '#64748b',
    fontSize: 15,
    marginTop: 24,
    textAlign: 'center',
  },
});

What Should Match?

This reference includes the three habit handlers, both summary cards, accessible row controls, and every style introduced in this step. It keeps the list in memory until the next step adds persistence.

Your tracker now responds to every core habit action and reports progress immediately. Next, you will preserve that working list when the browser reloads.

Keep Habits After a Restart

Your tracker can add, complete, count, and delete habits while the page stays open. A browser reload still clears the array because React state lives only in memory.

Persistent Async Storage gives that array a home between sessions. It stores structured data as JSON under one key.

A startup guard waits for saved habits before the app begins writing changes. Every later update keeps browser storage aligned with the current list.

In this step, get ready to:
  • Install Async Storage in the existing Expo project.
  • Load saved habits before displaying the list.
  • Save every habit change and confirm it survives a reload.
Install Async Storage

Async Storage provides persistent key-value storage across the platforms supported by your app. Expo SDK 57 installs compatible version 2.2.0 for this project.

  • Return to the PowerShell terminal in VS Code.
  • Stop the running Expo development server by pressing Ctrl+C.
  • Install Async Storage by running this command:
npx expo install @react-native-async-storage/async-storage

What does this command do?

The Expo installer adds @react-native-async-storage/async-storage to the project with the version compatible with Expo SDK 57. This keeps the JavaScript package aligned with the rest of the Expo project.

  • Restart the browser preview by running this command:
npx expo start --web

What does this command do?

This starts the Expo development server in web mode. The first restart can take up to a minute while the new dependency is bundled.

  • Confirm the browser displays Pocket Habit Tracker with its input and summary cards.

Good progress. The storage package is installed without changing how your tracker currently behaves.

Having trouble installing Async Storage?

  • Check that the PowerShell terminal is still inside the pocket-habit-tracker folder.
  • Confirm your internet connection before repeating the installation command.
  • Help me troubleshoot the Async Storage installation in my Expo project.
Load saved habits on startup

Loading must finish before saving begins. The isReady flag prevents the initial empty array from replacing habits that are already stored.

  • In App.js, select the opening imports through the existing habits state declaration.
  • Replace that opening with this storage-ready version:
import { useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';

const STORAGE_KEY = '@pocket-habit-tracker/habits';

export default function App() {
  const [habitName, setHabitName] = useState('');
  const [habits, setHabits] = useState([]);
  const [isReady, setIsReady] = useState(false);

What does this setup add?

  • The useEffect import lets the component run storage work after it starts.
  • The AsyncStorage import provides the methods that read and write saved strings.
  • The STORAGE_KEY constant gives this habit list a consistent address in browser storage.
  • The isReady state records when the initial storage check has finished.
  • Save App.js.
  • Confirm the browser still shows the habit tracker without an error overlay.

Seeing an import error?

  • Check that the Async Storage import matches the installed package name exactly.
  • Confirm the React import includes both useEffect and useState.
  • Help me fix the imports in App.js.

The first effect reads the stored string once when the component starts. It restores the array only after confirming the parsed value is an array.

  • Insert this effect below the isReady state declaration:
  useEffect(() => {
    let isMounted = true;
    async function loadHabits() {
      try {
        const savedHabits = await AsyncStorage.getItem(STORAGE_KEY);
        if (savedHabits !== null && isMounted) {
          const parsedHabits = JSON.parse(savedHabits);
          if (Array.isArray(parsedHabits)) setHabits(parsedHabits);
        }
      } catch (error) {
        console.error('Could not load habits.', error);
      } finally {
        if (isMounted) setIsReady(true);
      }
    }
    loadHabits();
    return () => { isMounted = false; };
  }, []);

How does loading work?

  • The empty dependency array makes this effect run once when the component starts.
  • The getItem call retrieves the string stored under STORAGE_KEY.
  • The JSON.parse call turns that string back into an array of habit objects.
  • The cleanup function prevents the effect from updating state after the component has been removed.
  • Save App.js.
  • Reload the browser to run the new startup effect.

The tracker returns without an error overlay. The empty-state message appears after the storage check finishes.

Does the app stay on an error screen?

  • Check that loadHabits sits inside the first useEffect call.
  • Confirm every opening brace in the effect has a matching closing brace.
  • Help me debug the loadHabits effect.

The interface needs a loading state while the storage check runs. This keeps the empty message hidden until the app knows whether saved habits exist.

  • In App.js, select the current return block.
  • Replace it with this loading-aware interface:
  return (
    <View style={styles.screen}><View style={styles.card}>
      <Text style={styles.eyebrow}>DAILY ROUTINE</Text><Text style={styles.title}>Pocket Habit Tracker</Text>
      <Text style={styles.subtitle}>Add one small action, then check it off when it is done.</Text>
      <View style={styles.inputRow}><TextInput accessibilityLabel="New habit name" onChangeText={setHabitName} onSubmitEditing={addHabit} placeholder="Example: Drink water" returnKeyType="done" style={styles.input} value={habitName}/><Pressable accessibilityLabel="Add habit" accessibilityRole="button" onPress={addHabit} style={({ pressed }) => [styles.addButton, pressed && styles.pressed]}><Text style={styles.addButtonText}>Add</Text></Pressable></View>
      <View style={styles.summaryRow}><View style={styles.summaryBox}><Text style={styles.summaryNumber}>{habits.length}</Text><Text style={styles.summaryLabel}>Total</Text></View><View style={styles.summaryBox}><Text style={styles.summaryNumber}>{completedCount}</Text><Text style={styles.summaryLabel}>Completed</Text></View></View>
      {!isReady ? <Text style={styles.emptyText}>Loading habits...</Text> : habits.length === 0 ? <Text style={styles.emptyText}>No habits yet. Add your first one.</Text> : <View style={styles.list}>{habits.map((habit) => <View key={habit.id} style={styles.habitRow}><Pressable accessibilityLabel={`Mark ${habit.name} as ${habit.completed ? 'active' : 'completed'}`} accessibilityRole="checkbox" accessibilityState={{ checked: habit.completed }} onPress={() => toggleHabit(habit.id)} style={({ pressed }) => [styles.habitMain, pressed && styles.pressed]}><Text style={[styles.statusBadge, habit.completed && styles.statusBadgeDone]}>{habit.completed ? 'Done' : 'Todo'}</Text><Text style={[styles.habitName, habit.completed && styles.habitNameDone]}>{habit.name}</Text></Pressable><Pressable accessibilityLabel={`Delete ${habit.name}`} accessibilityRole="button" onPress={() => deleteHabit(habit.id)} style={({ pressed }) => [styles.deleteButton, pressed && styles.pressed]}><Text style={styles.deleteButtonText}>Delete</Text></Pressable></View>)}</View>}
    </View></View>
  );

What changes in the interface?

The first condition displays Loading habits... while isReady is false. The existing empty state and habit rows take over after loading finishes.

  • Save App.js.
  • Reload the browser to confirm the tracker reaches its empty state after checking storage.

Stuck on Loading habits?

  • Confirm the finally block calls setIsReady(true).
  • Check the browser console for a storage or syntax error from loadHabits.
  • Help me find why isReady never becomes true.
Save every habit change

The second effect watches the current habit array after startup is complete. Each add, completion change, or deletion writes a fresh JSON string to the same storage key.

  • Insert this effect below the loading effect in App.js:
  useEffect(() => {
    if (!isReady) return;
    async function saveHabits() {
      try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(habits)); }
      catch (error) { console.error('Could not save habits.', error); }
    }
    saveHabits();
  }, [habits, isReady]);

How does saving work?

  • The readiness check blocks writes until the startup read has finished.
  • The JSON.stringify call converts the habit array into a string that Async Storage can store.
  • The effect runs again whenever habits changes.
  • Save App.js.

Before you reload the browser, do you expect the completed habit to return with the same status?

  • Enter Drink water in the new habit field.
  • Press the Add button.
  • Press the Todo control beside Drink water.
  • Reload the browser.

You should see Drink water return as Done. The summary should show 1 Total and 1 Completed.

That closes the persistence gap. Your tracker now restores both habit names and completion values after a restart.

Does the habit disappear after reload?

  • Confirm the save effect calls AsyncStorage.setItem with STORAGE_KEY.
  • Check that JSON.stringify receives the current habits array.
  • Help me debug habit persistence after a browser reload.

✔️ Awesome, I've got everything!

Your App.js now loads saved habits before writing changes. It also preserves completion state across browser reloads.

ⓧ I'd like to double check the full code

Here is the complete App.js for the persistent habit tracker.

import { useEffect, useState } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';

const STORAGE_KEY = '@pocket-habit-tracker/habits';

export default function App() {
  const [habitName, setHabitName] = useState('');
  const [habits, setHabits] = useState([]);
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    let isMounted = true;
    async function loadHabits() {
      try {
        const savedHabits = await AsyncStorage.getItem(STORAGE_KEY);
        if (savedHabits !== null && isMounted) {
          const parsedHabits = JSON.parse(savedHabits);
          if (Array.isArray(parsedHabits)) setHabits(parsedHabits);
        }
      } catch (error) {
        console.error('Could not load habits.', error);
      } finally {
        if (isMounted) setIsReady(true);
      }
    }
    loadHabits();
    return () => { isMounted = false; };
  }, []);

  useEffect(() => {
    if (!isReady) return;
    async function saveHabits() {
      try { await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(habits)); }
      catch (error) { console.error('Could not save habits.', error); }
    }
    saveHabits();
  }, [habits, isReady]);

  function addHabit() {
    const cleanName = habitName.trim();
    if (!cleanName) return;
    const newHabit = { id: `${Date.now()}-${Math.random()}`, name: cleanName, completed: false };
    setHabits((currentHabits) => [...currentHabits, newHabit]);
    setHabitName('');
  }

  function toggleHabit(id) {
    setHabits((currentHabits) => currentHabits.map((habit) => habit.id === id ? { ...habit, completed: !habit.completed } : habit));
  }

  function deleteHabit(id) {
    setHabits((currentHabits) => currentHabits.filter((habit) => habit.id !== id));
  }

  const completedCount = habits.filter((habit) => habit.completed).length;

  return (
    <View style={styles.screen}><View style={styles.card}>
      <Text style={styles.eyebrow}>DAILY ROUTINE</Text><Text style={styles.title}>Pocket Habit Tracker</Text>
      <Text style={styles.subtitle}>Add one small action, then check it off when it is done.</Text>
      <View style={styles.inputRow}><TextInput accessibilityLabel="New habit name" onChangeText={setHabitName} onSubmitEditing={addHabit} placeholder="Example: Drink water" returnKeyType="done" style={styles.input} value={habitName}/><Pressable accessibilityLabel="Add habit" accessibilityRole="button" onPress={addHabit} style={({ pressed }) => [styles.addButton, pressed && styles.pressed]}><Text style={styles.addButtonText}>Add</Text></Pressable></View>
      <View style={styles.summaryRow}><View style={styles.summaryBox}><Text style={styles.summaryNumber}>{habits.length}</Text><Text style={styles.summaryLabel}>Total</Text></View><View style={styles.summaryBox}><Text style={styles.summaryNumber}>{completedCount}</Text><Text style={styles.summaryLabel}>Completed</Text></View></View>
      {!isReady ? <Text style={styles.emptyText}>Loading habits...</Text> : habits.length === 0 ? <Text style={styles.emptyText}>No habits yet. Add your first one.</Text> : <View style={styles.list}>{habits.map((habit) => <View key={habit.id} style={styles.habitRow}><Pressable accessibilityLabel={`Mark ${habit.name} as ${habit.completed ? 'active' : 'completed'}`} accessibilityRole="checkbox" accessibilityState={{ checked: habit.completed }} onPress={() => toggleHabit(habit.id)} style={({ pressed }) => [styles.habitMain, pressed && styles.pressed]}><Text style={[styles.statusBadge, habit.completed && styles.statusBadgeDone]}>{habit.completed ? 'Done' : 'Todo'}</Text><Text style={[styles.habitName, habit.completed && styles.habitNameDone]}>{habit.name}</Text></Pressable><Pressable accessibilityLabel={`Delete ${habit.name}`} accessibilityRole="button" onPress={() => deleteHabit(habit.id)} style={({ pressed }) => [styles.deleteButton, pressed && styles.pressed]}><Text style={styles.deleteButtonText}>Delete</Text></Pressable></View>)}</View>}
    </View></View>
  );
}

const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: '#e8eef8', paddingHorizontal: 20, paddingTop: 48 }, card: { width: '100%', maxWidth: 560, alignSelf: 'center', backgroundColor: '#ffffff', borderRadius: 24, padding: 24 }, eyebrow: { color: '#4f46e5', fontSize: 12, fontWeight: '700', letterSpacing: 1.4 }, title: { color: '#172033', fontSize: 30, fontWeight: '800', marginTop: 6 }, subtitle: { color: '#64748b', fontSize: 16, lineHeight: 23, marginTop: 8 }, inputRow: { flexDirection: 'row', marginTop: 24 }, input: { flex: 1, borderColor: '#cbd5e1', borderWidth: 1, borderRadius: 12, color: '#172033', fontSize: 16, paddingHorizontal: 14, paddingVertical: 12 }, addButton: { justifyContent: 'center', backgroundColor: '#4f46e5', borderRadius: 12, marginLeft: 10, paddingHorizontal: 20 }, addButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '700' }, pressed: { opacity: 0.7 }, summaryRow: { flexDirection: 'row', marginTop: 20 }, summaryBox: { flex: 1, backgroundColor: '#f1f5f9', borderRadius: 14, padding: 14, marginRight: 10 }, summaryNumber: { color: '#172033', fontSize: 24, fontWeight: '800' }, summaryLabel: { color: '#64748b', fontSize: 13, marginTop: 2 }, list: { marginTop: 18 }, habitRow: { flexDirection: 'row', alignItems: 'center', borderColor: '#e2e8f0', borderTopWidth: 1, paddingVertical: 12 }, habitMain: { flex: 1, flexDirection: 'row', alignItems: 'center' }, statusBadge: { width: 48, color: '#475569', fontSize: 12, fontWeight: '700' }, statusBadgeDone: { color: '#15803d' }, habitName: { flex: 1, color: '#172033', fontSize: 16 }, habitNameDone: { color: '#94a3b8', textDecorationLine: 'line-through' }, deleteButton: { backgroundColor: '#fee2e2', borderRadius: 10, marginLeft: 12, paddingHorizontal: 12, paddingVertical: 8 }, deleteButtonText: { color: '#b91c1c', fontSize: 13, fontWeight: '700' }, emptyText: { color: '#64748b', fontSize: 15, marginTop: 24, textAlign: 'center' } });

What should this reference contain?

The complete file contains one startup effect and one saving effect. The existing habit controls and styles remain unchanged.

Secret mission

Filter the Habit List

A growing habit list can make today's priorities hard to spot. Add All, Active, and Done filters so you can focus the list without changing the habits saved in browser storage.

Clean Up Your Resources

Clean Up Your Resources

Your habit tracker runs locally through Expo in a browser. Neither the project folder nor the saved browser data creates an ongoing cost.

Choose whether to keep everything available, pause the development server, or delete the local resources entirely. No cloud resources, store listings, or paid EAS services were created.

Resources you used:

  • The local pocket-habit-tracker project folder.
  • Browser storage containing the complete habit array under @pocket-habit-tracker/habits.
  • The running Expo development server in PowerShell.

Keep everything running

No action is needed. Choose this option if you want to keep building your habit tracker.

  • Leave the Expo development server running.
  • Keep the pocket-habit-tracker folder for future changes.
  • Keep the browser storage entry to preserve every saved habit.

Pause - I'll come back to this later

Stopping the development server frees local memory. Your project files remain on your computer.

  • Switch back to the PowerShell terminal in Visual Studio Code.
  • Press Ctrl+C to stop the Expo development server.

That's a clean stopping point. Your saved habits remain available for your next session.

  • Return to the pocket-habit-tracker terminal in Visual Studio Code when you are ready to continue.
  • Restart the browser preview by running this command:
npx expo start --web

What does this command do?

This starts the Expo development server for the existing project. The web option opens the React Native app in your browser.

You'll see the browser preview open. Your saved habits return from browser storage.

Preview Does Not Restart?

  • Confirm that the terminal path ends with pocket-habit-tracker.
  • Use this if you need help diagnosing the restart: Help me restart my Expo browser preview.

Delete - I don't want to use this again

Permanent deletion removes your habit tracker code from your computer. Clearing the habits first empties the saved browser list before you remove the project folder.

  • Return to the running habit tracker browser tab from earlier.
  • Select All in the filter row.
  • Press Delete beside every saved habit.

After the final row disappears, you'll see No habits match this filter. That's the saved browser data cleared.

  • Switch back to the PowerShell terminal in Visual Studio Code.
  • Press Ctrl+C to stop the Expo development server.

The app is now stopped. You can remove its folder without leaving the development server active.

  • Close Visual Studio Code.
  • Press the Windows key to open Windows search.
  • Type File Explorer and press Enter to open it.
  • Navigate to the folder that contains pocket-habit-tracker.
  • Select the pocket-habit-tracker folder.
  • Press Shift+Delete to remove the folder permanently.
  • Confirm the permanent deletion when Windows asks.

The pocket-habit-tracker folder no longer appears in File Explorer. No local project files remain.

Nice Work!

Nice Work!

Your Pocket Habit Tracker is complete! You built a persistent app with React Native that responds to habit actions while protecting the full list across reloads.

You've learned how to:

  • Built a cross-platform habit interface from reusable core components. Added a controlled text input plus accessible controls.
  • Managed habit data with React state. Used immutable array updates for every habit action. Derived live progress totals from the current list.
  • Diagnosed the difference between temporary state and persistent storage. Saved the full habit array with Async Storage plus JSON serialization. Restored each habit after a reload. Preserved its completion status.
  • Secret Mission: Added All, Active, and Done filters. Derived each view without changing the stored habit list.

Ready to quiz yourself?