
Complete Guide to React Native Building a Feature Rich Todo App
After working in the React ecosystem for a long time, I decided to venture into mobile app development. React Native was particularly appealing because it offered the ability to develop native mobile applications using my existing JavaScript and React knowledge. In this post, I'll detail my experience building a feature-rich Todo app as my first React Native project.
What is React Native?
React Native is a framework developed by Facebook (now Meta) that allows you to create mobile applications using JavaScript and React. Unlike other frameworks that use WebViews, React Native transforms your JavaScript code into native components for iOS and Android. This means your applications look, feel, and perform as if they were written in the platform's native language.
The advantages of React Native include:
- Learn once, write anywhere: You can use your existing React knowledge
- Native performance: Direct access to native platform APIs
- Hot reloading: See changes instantly during development
- Large community: Extensive libraries and support
- Cross-platform: Build for iOS and Android from a single codebase
Setting Up the Development Environment
To get started with React Native, you first need to set up your development environment. I chose to use Expo, which simplifies the setup process and provides a set of tools and services for React Native development.
Here's how I set up my environment:
// Install Expo CLI globally
npm install -g expo-cli
// Create a new project
expo init TodoApp
// Choose a template (I selected the blank template)
// Navigate to the project directory
cd TodoApp
// Start the development server
npm start
With Expo, I could preview the app directly on my physical device by scanning a QR code, which made testing much easier.
Project Structure and Navigation
For my Todo app, I implemented a more complex structure with multiple screens and navigation:
// App.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { ThemeProvider, useTheme } from './contexts/ThemeContext';
import HomeScreen from './screens/HomeScreen';
import SettingsScreen from './screens/SettingsScreen';
import TaskDetailsScreen from './screens/TaskDetailsScreen';
import CategoryScreen from './screens/CategoryScreen';
import { MaterialIcons } from '@expo/vector-icons';
import { StatusBar } from 'react-native';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
function HomeStack() {
const { theme } = useTheme();
return (
<Stack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: theme.card,
},
headerTintColor: theme.text,
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
<Stack.Screen name="TaskList" component={HomeScreen} options={{ title: 'Tasks' }} />
<Stack.Screen name="TaskDetails" component={TaskDetailsScreen} options={{ title: 'Task Details' }} />
</Stack.Navigator>
);
}
// Settings stack for settings and category management
function SettingsStack() {
const { theme } = useTheme();
return (
<Stack.Navigator screenOptions={{ /* Similar header styling */ }}>
<Stack.Screen name="SettingsScreen" component={SettingsScreen} options={{ title: 'Settings' }} />
<Stack.Screen name="Categories" component={CategoryScreen} options={{ title: 'Categories' }} />
</Stack.Navigator>
);
}
function TabNavigator() {
const { theme, darkMode } = useTheme();
return (
<>
<StatusBar
barStyle={darkMode ? "light-content" : "dark-content"}
backgroundColor={theme.background}
/>
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName = route.name === 'Home' ? 'list' : 'settings';
return <MaterialIcons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: theme.primary,
tabBarInactiveTintColor: theme.secondaryText,
tabBarStyle: {
backgroundColor: theme.card,
borderTopColor: theme.border,
},
headerShown: false,
})}
>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Settings" component={SettingsStack} />
</Tab.Navigator>
</>
);
}
export default function App() {
return (
<ThemeProvider>
<NavigationContainer>
<TabNavigator />
</NavigationContainer>
</ThemeProvider>
);
}
I used React Navigation to implement both tab navigation (for switching between the task list and settings) and stack navigation (for navigating to task details and category management). This provides a native-feeling navigation experience for users.
Theme and App Context with React Context API
One of the most powerful features of my Todo app is the comprehensive context system that manages both theming and application state:
// contexts/ThemeContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
// Theme colors
const themes = {
light: {
background: '#f5f5f5',
card: 'white',
text: '#333333',
secondaryText: '#757575',
primary: '#2196F3',
border: '#dddddd',
danger: '#F44336',
success: '#4CAF50',
warning: '#FFC107',
},
dark: {
background: '#121212',
card: '#1e1e1e',
text: '#ffffff',
secondaryText: '#aaaaaa',
primary: '#2196F3',
border: '#333333',
danger: '#F44336',
success: '#4CAF50',
warning: '#FFC107',
},
};
// Default categories
export const defaultCategories = [
{ id: '1', name: 'Personal', color: '#FF5733' },
{ id: '2', name: 'Work', color: '#33FF57' },
{ id: '3', name: 'Shopping', color: '#3357FF' },
{ id: '4', name: 'Health', color: '#FF33F5' },
{ id: '5', name: 'Education', color: '#33FFF5' },
];
// Theme context
const ThemeContext = createContext();
// App context for task management
export const AppContext = createContext();
export const ThemeProvider = ({ children }) => {
const [darkMode, setDarkMode] = useState(false);
const [theme, setTheme] = useState(themes.light);
const [tasks, setTasks] = useState([]);
const [categories, setCategories] = useState(defaultCategories);
const [loading, setLoading] = useState(true);
// Load saved data
useEffect(() => {
loadThemePreference();
loadTasks();
loadCategories();
}, []);
// Toggle theme between light and dark mode
const toggleTheme = async () => {
const newDarkMode = !darkMode;
setDarkMode(newDarkMode);
setTheme(newDarkMode ? themes.dark : themes.light);
try {
await AsyncStorage.setItem('@theme_preference', JSON.stringify(newDarkMode));
} catch (error) {
console.error('Error saving theme preference:', error);
}
};
// Task management functions
const addTask = async (newTask) => {
try {
const taskWithDefaults = {
id: Date.now().toString(),
completed: false,
createdAt: new Date().toISOString(),
...newTask
};
const updatedTasks = [...tasks, taskWithDefaults];
await AsyncStorage.setItem('@todo_items', JSON.stringify(updatedTasks));
setTasks(updatedTasks);
return true;
} catch (error) {
console.error('Error adding task:', error);
return false;
}
};
// Other functions for task and category management...
return (
<ThemeContext.Provider value={{ theme, darkMode, toggleTheme }}>
<AppContext.Provider value={{
tasks,
categories,
loading,
addTask,
updateTask,
deleteTask,
clearAllTasks,
addCategory,
updateCategory,
deleteCategory,
loadTasks
}}>
{children}
</AppContext.Provider>
</ThemeContext.Provider>
);
};
// Custom hooks
export const useTheme = () => useContext(ThemeContext);
export const useAppContext = () => useContext(AppContext);
This context system provides:
- Theme Management: Light and dark mode with persistent user preference
- Task Management: CRUD operations for tasks with AsyncStorage persistence
- Category Management: Create, update, and delete task categories
- Custom Hooks: Easy access to theme and app functionality throughout the app
Home Screen - Task List and Creation
The main screen of the app displays the task list and allows users to add new tasks:
// screens/HomeScreen.js
import React, { useState } from "react";
import {
StyleSheet,
Text,
View,
SafeAreaView,
TextInput,
TouchableOpacity,
FlatList,
Keyboard,
} from "react-native";
import { useTheme, useAppContext } from '../contexts/ThemeContext';
export default function HomeScreen({ navigation }) {
// Get theme and app context
const { theme } = useTheme();
const { tasks, loading, addTask, updateTask, deleteTask } = useAppContext();
// State for new task input
const [taskText, setTaskText] = useState("");
// View task details
const viewTaskDetails = (task) => {
navigation.navigate("TaskDetails", {
task,
});
};
// Toggle task completion status
const toggleComplete = (taskId) => {
const task = tasks.find(t => t.id === taskId);
if (task) {
updateTask(taskId, { completed: !task.completed });
}
};
// Add new task
const handleAddTask = () => {
if (taskText.trim().length === 0) {
return;
}
// Add new task
addTask({
text: taskText,
categoryId: null,
notes: "",
});
setTaskText("");
Keyboard.dismiss();
};
// Render task item
const renderItem = ({ item }) => (
<TouchableOpacity
style={[styles.taskContainer, { backgroundColor: theme.card, borderColor: theme.border }]}
onPress={() => viewTaskDetails(item)}
>
<TouchableOpacity
style={styles.taskTextContainer}
onPress={(e) => {
e.stopPropagation();
toggleComplete(item.id);
}}
>
<View
style={[
styles.checkbox,
{ borderColor: theme.primary },
item.completed && { backgroundColor: theme.primary }
]}
/>
<Text
style={[
styles.taskText,
{ color: theme.text },
item.completed && { textDecorationLine: 'line-through', color: theme.secondaryText }
]}
>
{item.text}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.deleteButton, { backgroundColor: theme.danger }]}
onPress={(e) => {
e.stopPropagation();
deleteTask(item.id);
}}
>
<Text style={styles.deleteButtonText}>Delete</Text>
</TouchableOpacity>
</TouchableOpacity>
);
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.content}>
{/* Task input form */}
<View style={styles.inputContainer}>
<TextInput
style={[styles.input, { backgroundColor: theme.card, borderColor: theme.border, color: theme.text }]}
placeholder="Add a new task..."
placeholderTextColor={theme.secondaryText}
value={taskText}
onChangeText={setTaskText}
/>
<TouchableOpacity
style={[styles.addButton, { backgroundColor: theme.primary }]}
onPress={handleAddTask}
>
<Text style={styles.addButtonText}>Add</Text>
</TouchableOpacity>
</View>
{/* Task list */}
{tasks.length > 0 ? (
<FlatList
data={tasks}
renderItem={renderItem}
keyExtractor={(item) => item.id}
style={styles.list}
/>
) : (
<Text style={[styles.emptyText, { color: theme.secondaryText }]}>No tasks added yet.</Text>
)}
</View>
</SafeAreaView>
);
}
// Styles...
The HomeScreen component includes:
- Task Input: A form for adding new tasks
- Task List: A FlatList displaying all tasks with completion status
- Task Actions: Toggle completion and delete functionality
- Navigation: Tap on a task to view its details
- Theming: Dynamic styling based on the current theme
Task Details Screen
The TaskDetailsScreen allows users to view and edit task details:
// screens/TaskDetailsScreen.js
import React, { useState } from 'react';
import {
StyleSheet,
Text,
View,
TextInput,
TouchableOpacity,
Switch,
ScrollView,
Alert
} from 'react-native';
import { useTheme, useAppContext } from '../contexts/ThemeContext';
import { Picker } from '@react-native-picker/picker';
export default function TaskDetailsScreen({ route, navigation }) {
const { task } = route.params;
const { theme } = useTheme();
const { updateTask, categories } = useAppContext();
const [text, setText] = useState(task.text);
const [completed, setCompleted] = useState(task.completed);
const [notes, setNotes] = useState(task.notes || '');
const [categoryId, setCategoryId] = useState(task.categoryId || null);
// Save changes and go back
const saveChanges = () => {
if (text.trim().length === 0) {
Alert.alert('Error', 'Task text cannot be empty');
return;
}
const updatedTask = {
...task,
text,
completed,
notes,
categoryId
};
updateTask(task.id, updatedTask);
navigation.goBack();
};
// Get category color
const getCategoryColor = () => {
if (!categoryId) return null;
const category = categories.find(c => c.id === categoryId);
return category ? category.color : null;
};
return (
<ScrollView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.content}>
{/* Task text input */}
<Text style={[styles.label, { color: theme.text }]}>Task</Text>
<TextInput
style={[styles.input, { backgroundColor: theme.card, borderColor: theme.border, color: theme.text }]}
value={text}
onChangeText={setText}
placeholder="Task text"
placeholderTextColor={theme.secondaryText}
/>
{/* Completion status */}
<View style={styles.switchContainer}>
<Text style={[styles.label, { color: theme.text }]}>Completed</Text>
<Switch
value={completed}
onValueChange={setCompleted}
trackColor={{ false: theme.border, true: theme.primary }}
thumbColor={completed ? theme.success : '#f4f3f4'}
/>
</View>
{/* Category selection */}
<Text style={[styles.label, { color: theme.text }]}>Category</Text>
<View style={[styles.pickerContainer, { backgroundColor: theme.card, borderColor: theme.border }]}>
<Picker
selectedValue={categoryId}
onValueChange={(itemValue) => setCategoryId(itemValue)}
style={{ color: theme.text }}
dropdownIconColor={theme.text}
>
<Picker.Item label="No Category" value={null} />
{categories.map(category => (
<Picker.Item
key={category.id}
label={category.name}
value={category.id}
color={category.color}
/>
))}
</Picker>
</View>
{/* Notes */}
<Text style={[styles.label, { color: theme.text }]}>Notes</Text>
<TextInput
style={[styles.notesInput, { backgroundColor: theme.card, borderColor: theme.border, color: theme.text }]}
value={notes}
onChangeText={setNotes}
placeholder="Add notes here..."
placeholderTextColor={theme.secondaryText}
multiline
textAlignVertical="top"
/>
{/* Action buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, styles.cancelButton, { backgroundColor: theme.border }]}
onPress={() => navigation.goBack()}
>
<Text style={[styles.buttonText, { color: theme.text }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.saveButton, { backgroundColor: theme.primary }]}
onPress={saveChanges}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
);
}
// Styles...
The TaskDetailsScreen provides:
- Task Editing: Edit the task text
- Completion Toggle: Mark tasks as completed or incomplete
- Category Assignment: Assign tasks to different categories
- Notes: Add detailed notes to tasks
- Validation: Ensure task text is not empty
Settings and Category Management
The app also includes settings and category management screens:
// screens/SettingsScreen.js (partial)
export default function SettingsScreen({ navigation }) {
const { theme, darkMode, toggleTheme } = useTheme();
const { tasks, clearAllTasks } = useAppContext();
// Confirm and clear all tasks
const confirmClearTasks = () => {
Alert.alert(
'Clear All Tasks',
'Are you sure you want to delete all tasks? This action cannot be undone.',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Clear All',
onPress: clearAllTasks,
style: 'destructive',
},
]
);
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.background }]}>
<View style={styles.content}>
{/* Theme toggle */}
<View style={[styles.settingItem, { borderBottomColor: theme.border }]}>
<Text style={[styles.settingText, { color: theme.text }]}>Dark Mode</Text>
<Switch
value={darkMode}
onValueChange={toggleTheme}
trackColor={{ false: theme.border, true: theme.primary }}
thumbColor={darkMode ? theme.success : '#f4f3f4'}
/>
</View>
{/* Category management */}
<TouchableOpacity
style={[styles.settingItem, { borderBottomColor: theme.border }]}
onPress={() => navigation.navigate('Categories')}
>
<Text style={[styles.settingText, { color: theme.text }]}>Manage Categories</Text>
<MaterialIcons name="chevron-right" size={24} color={theme.secondaryText} />
</TouchableOpacity>
{/* Clear all tasks */}
<TouchableOpacity
style={[styles.settingItem, { borderBottomColor: theme.border }]}
onPress={confirmClearTasks}
disabled={tasks.length === 0}
>
<Text
style={[
styles.settingText,
{ color: tasks.length > 0 ? theme.danger : theme.secondaryText }
]}
>
Clear All Tasks
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
The settings screen provides:
- Theme Toggle: Switch between light and dark mode
- Category Management: Navigate to the category management screen
- Clear All Tasks: Option to delete all tasks with confirmation
Key Differences from Web Development
While developing this app, I noticed several important differences between React Native and web development:
1. Styling System
React Native uses a styling system similar to CSS, but with some significant differences:
- No CSS files - styles are defined using JavaScript objects
- Limited set of style properties compared to CSS
- No cascading - styles don't inherit from parent to child
- Flexbox is the primary layout mechanism, but with some differences from web
// Style definition in React Native
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
text: {
fontSize: 18,
color: '#333',
fontWeight: 'bold',
},
});
2. Platform-Specific Components
React Native uses its own set of UI components that map to native platform components:
// Web (HTML)
<div className="container">
<p className="text">Hello World</p>
<input type="text" placeholder="Type here" />
</div>
// React Native
<View style={styles.container}>
<Text style={styles.text}>Hello World</Text>
<TextInput placeholder="Type here" />
</View>
Key Component Replacements:
| React Native | HTML Equivalent |
|---|---|
View | div |
Text | p, span |
TextInput | input |
TouchableOpacity | Click interactions |
3. Navigation
Web applications use URLs for navigation, while React Native apps require a navigation library. I used React Navigation, which provides a native-feeling navigation experience:
// Stack navigation
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
// Tab navigation
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Settings" component={SettingsStack} />
</Tab.Navigator>
4. Platform-Specific Code
Sometimes you need to write platform-specific code to handle differences between iOS and Android:
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
marginTop: Platform.OS === 'ios' ? 50 : 20,
paddingBottom: Platform.OS === 'ios' ? 30 : 20,
// Other styles...
},
});
// Or conditional code blocks
{Platform.OS === 'ios' ? (
<View style={styles.iosSpecific}>
<Text>This is visible only on iOS</Text>
</View>
) : (
<View style={styles.androidSpecific}>
<Text>This is visible only on Android</Text>
</View>
)}
Challenges I Faced
While developing this project, my first React Native app, I encountered several challenges:
1. Navigation Setup
Setting up navigation with React Navigation was initially confusing. Understanding the differences between stack, tab, and drawer navigation took some time. The documentation was helpful, but I had to experiment to get the right configuration.
2. Theme Implementation
Implementing a theme system that affects all components required careful planning. I had to ensure that all components received the theme context and applied the appropriate styles. Using a context provider at the root of the application made this possible.
3. Form Controls
React Native's form controls are quite different from web form elements. For example, there's no built-in select/dropdown component, so I had to use the Picker component from @react-native-picker/picker. Similarly, handling multiline text input required specific properties.
4. AsyncStorage Management
Managing data persistence with AsyncStorage required a different approach than web localStorage. Since AsyncStorage operations are asynchronous, I had to properly handle promises and potential errors.
Lessons Learned
Through this project, I learned several valuable lessons:
1- Plan Your Navigation: Decide on your navigation structure early, as it affects the entire app architecture.
2- Use Context Effectively: React Context is powerful for managing global state like themes and app data.
3- Consider Platform Differences: Always be aware of platform-specific behaviors and test on both iOS and Android.
4- Optimize List Rendering: For better performance, use FlatList instead of mapping over arrays, especially for long lists.
5- Implement Data Persistence Early: Plan your data storage strategy from the beginning to avoid refactoring later.
Conclusion
Developing a Todo app with React Native has been an exciting journey. Using my existing React knowledge to explore the world of mobile development has opened up new possibilities for creating cross-platform applications.
The app I built goes beyond a simple todo list, incorporating features like theme switching, category management, and detailed task editing. These features demonstrate the power and flexibility of React Native for building sophisticated mobile applications.
If you're a web developer considering mobile development, React Native offers a familiar entry point with a relatively gentle learning curve. Seeing your app running on an actual mobile device is incredibly satisfying.
You can find the complete source code for my Todo app on GitHub. Feel free to explore, fork, and use it as a starting point for your own React Native journey.
Have you tried React Native? What was your experience like? Let me know in the comments below!
Note for developers: The code snippets in this blog post are simplified to highlight the key concepts and structure of the application. For a complete, working implementation with all necessary functions, styles, and components, please check out the full source code on GitHub.











