← All phases
4

Lesson 4 — Building the Core Feature

Maps to: SYSTEM.md Phase 4 (expo-build) Goal: leave this lesson with your app's main feature actually working — not a mockup, real functioning code.

How this works

You describe the feature in plain English. Claude Code writes the actual TypeScript/React code. You read what it wrote, test it on your phone, and ask for fixes in plain English too. You are not writing this code by hand — but you should be able to read it, which is exactly what 03-software-engineering-fundamentals.md prepared you for.

Describe the feature to Claude Code

Build the core feature for HabitTracker:
- A Zustand store that holds a list of habits, each with a name, a streak count, and the date it was last completed
- A home screen (app/index.tsx) that lists all habits, shows each one's streak, and lets me tap a habit to mark it done for today
- A second screen (app/add.tsx) with a text input and Save button to add a new habit, then goes back to the home screen

What Claude Code builds (read this, don't just trust it)

The Zustand storestore/habitStore.ts:

import { create } from 'zustand';

type Habit = {
  id: string;
  name: string;
  streak: number;
  lastCompletedDate: string | null;
};

type HabitStore = {
  habits: Habit[];
  addHabit: (name: string) => void;
  completeToday: (id: string) => void;
};

export const useHabitStore = create<HabitStore>((set) => ({
  habits: [],
  addHabit: (name) =>
    set((state) => ({
      habits: [
        ...state.habits,
        { id: Date.now().toString(), name, streak: 0, lastCompletedDate: null },
      ],
    })),
  completeToday: (id) =>
    set((state) => ({
      habits: state.habits.map((habit) =>
        habit.id === id
          ? {
              ...habit,
              streak: habit.streak + 1,
              lastCompletedDate: new Date().toISOString().slice(0, 10),
            }
          : habit
      ),
    })),
}));

Read this like 03-software-engineering-fundamentals.md taught you: habits is an array, addHabit and completeToday are functions, and set is how Zustand updates state (similar to what let/reassignment does for a plain variable, but for a shared store multiple screens can read from).

The home screenapp/index.tsx:

import { View, Text, FlatList, Pressable, StyleSheet } from 'react-native';
import { Link } from 'expo-router';
import { useHabitStore } from '../store/habitStore';

export default function HabitList() {
  const habits = useHabitStore((state) => state.habits);
  const completeToday = useHabitStore((state) => state.completeToday);
  const today = new Date().toISOString().slice(0, 10);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>My Habits</Text>
      <FlatList
        data={habits}
        keyExtractor={(habit) => habit.id}
        renderItem={({ item }) => {
          const doneToday = item.lastCompletedDate === today;
          return (
            <Pressable style={styles.row} onPress={() => !doneToday && completeToday(item.id)}>
              <Text style={styles.habitName}>{item.name}</Text>
              <Text style={styles.streak}>{doneToday ? '✓ done today' : `🔥 ${item.streak}`}</Text>
            </Pressable>
          );
        }}
        ListEmptyComponent={<Text>No habits yet — add one below.</Text>}
      />
      <Link href="/add" style={styles.addButton}>
        <Text style={styles.addButtonText}>+ Add Habit</Text>
      </Link>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20, paddingTop: 60 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 20 },
  row: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    padding: 16,
    backgroundColor: '#f0f0f0',
    borderRadius: 10,
    marginBottom: 10,
  },
  habitName: { fontSize: 16 },
  streak: { fontSize: 16, fontWeight: '600' },
  addButton: { backgroundColor: '#4f46e5', padding: 16, borderRadius: 10, alignItems: 'center', marginTop: 10 },
  addButtonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

The add-habit screenapp/add.tsx:

import { useState } from 'react';
import { View, Text, TextInput, Pressable, StyleSheet } from 'react-native';
import { router } from 'expo-router';
import { useHabitStore } from '../store/habitStore';

export default function AddHabit() {
  const [name, setName] = useState('');
  const addHabit = useHabitStore((state) => state.addHabit);

  const handleSave = () => {
    if (name.trim().length === 0) return;
    addHabit(name.trim());
    router.back();
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>New Habit</Text>
      <TextInput style={styles.input} placeholder="e.g. Drink water" value={name} onChangeText={setName} autoFocus />
      <Pressable style={styles.saveButton} onPress={handleSave}>
        <Text style={styles.saveButtonText}>Save</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 20, paddingTop: 60 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20 },
  input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 10, padding: 14, fontSize: 16, marginBottom: 20 },
  saveButton: { backgroundColor: '#4f46e5', padding: 16, borderRadius: 10, alignItems: 'center' },
  saveButtonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

Fundamentals check (from 03-software-engineering-fundamentals.md)

Before moving on, answer these about the code above:

Test it on your phone

Scan the QR code, tap "+ Add Habit," type a habit name, save, and tap it to mark it done. The streak should show 🔥 1. Try marking it done a second time the same day — nothing should change (the store only updates lastCompletedDate, so doneToday stays true).

Deliverable

Your app's main feature works — the exact deliverable 04-workflow-course-overview.md Module 4 asks for.

Apply to your own idea

Describe your own app's core feature to Claude Code the same way: what state does it need to remember, what should the main screen show and let you do, and (if needed) what a second screen handles. Read whatever Claude Code writes back using the same fundamentals check above before testing it.

Prompts

Maps to: courses/react-native-course/lessons/04-core-feature.md (SYSTEM.md Phase 4)

Prompt 1 — Build the feature

Build the core feature for {{APP_NAME}}:
{{DESCRIBE WHAT STATE THE APP NEEDS TO REMEMBER, WHAT THE MAIN SCREEN SHOWS AND LETS THE USER DO, AND (IF NEEDED) WHAT A SECOND SCREEN HANDLES — pull this straight from concept.md}}

After Claude Code responds

Read what it wrote back before testing it — this is where 03-software-engineering-fundamentals.md's fundamentals check matters: is each variable a let or const? What type does it hold? Is there a function? Is there a decision (if) being made?

Prompt 2 — If something's wrong

Describe the bug in plain English, specifically:

{{WHAT'S ACTUALLY HAPPENING}} — for example: "tapping the plus button does nothing"

You don't need to point at a line of code. "The button does nothing" is specific enough.

Full lesson

courses/react-native-course/lessons/04-core-feature.md