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 store — store/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 screen — app/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 screen — app/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:
- In
habitStore.ts, ishabitsaletorconst-style binding? (It's insidecreate<HabitStore>((set) => ({...}))— the store itself isconst, but the values inside it change viaset.) - What type does
streakhold? (number.) - In
AddHabit, what does theif (name.trim().length === 0) return;line do? (Anifwith noelse— a decision with only one path: stop early if the input is blank.)
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.