Saving Tasks to Local Storage
In this tutorial, we’ll implement functionality to save tasks to the device’s local storage, allowing them to persist between app sessions. We’ll use React Native’s AsyncStorage to achieve this.
Step 1: Install the Package
Section titled “Step 1: Install the Package”npx expo install @react-native-async-storage/async-storage
Step 2: Import AsyncStorage and Define a Storage Key
Section titled “Step 2: Import AsyncStorage and Define a Storage Key”In your main file (app/index.tsx
):
import * as React from "react";import { ScrollView, View } from "react-native";import AsyncStorage from "@react-native-async-storage/async-storage";import AddTask from "~/components/AddTask";import Task from "~/components/Task";import { Text } from "~/components/ui/text";
interface TaskItem { id: number; title: string; category: string; isChecked: boolean;}
// Key for storing tasks in AsyncStorageconst TASKS_STORAGE_KEY = "hallpass_tasks";
Step 3: Load Tasks from Storage on App Start
Section titled “Step 3: Load Tasks from Storage on App Start”export default function HomeScreen() { const [tasks, setTasks] = React.useState<TaskItem[]>([ { id: 1, title: "Task 1", category: "Category 1", isChecked: false }, { id: 2, title: "Task 2", category: "Category 2", isChecked: true }, { id: 3, title: "Task 3", category: "Category 3", isChecked: false }, { id: 4, title: "Task 4", category: "Category 2", isChecked: true }, ]); const [tasks, setTasks] = React.useState<TaskItem[]>([]); const [isLoading, setIsLoading] = React.useState(true);
// Load tasks from storage when app starts React.useEffect(() => { const loadTasks = async () => { try { const storedTasks = await AsyncStorage.getItem(TASKS_STORAGE_KEY); if (storedTasks !== null) { setTasks(JSON.parse(storedTasks)); } } catch (error) { console.error("Failed to load tasks:", error); } finally { setIsLoading(false); } };
loadTasks(); }, []);
Step 4: Create a Function to Save Tasks
Section titled “Step 4: Create a Function to Save Tasks”Add this code to your main file. It will load tasks from storage when the app starts:
// Save tasks to storage whenever they changeconst saveTasks = async (updatedTasks: TaskItem[]) => { try { await AsyncStorage.setItem( TASKS_STORAGE_KEY, JSON.stringify(updatedTasks) ); } catch (error) { console.error("Failed to save tasks:", error); }};
Step 5: Update Task Handlers to Save Changes
Section titled “Step 5: Update Task Handlers to Save Changes”const handleAddTask = (title: string, category: string) => { const nextId = tasks.length > 0 ? Math.max(...tasks.map((t) => t.id)) + 1 : 1; setTasks([...tasks, { id: nextId, title, category, isChecked: false }]); const updatedTasks = [ ...tasks, { id: nextId, title, category, isChecked: false }, ]; setTasks(updatedTasks); saveTasks(updatedTasks); };
const handleTaskUpdate = (updatedTask: TaskItem) => { const updatedTasks = tasks.map((task) => task.id === updatedTask.id ? updatedTask : task ); setTasks(updatedTasks); saveTasks(updatedTasks);};
Step 6: Update the UI to Handle Loading State
Section titled “Step 6: Update the UI to Handle Loading State”<ScrollView contentContainerStyle={{ paddingHorizontal: 24, paddingVertical: 16, }}> {tasks.map((task) => ( <Task key={task.id} task={task} /> ))} {isLoading ? ( <Text className="text-center text-foreground text-lg"> Loading tasks... </Text> ) : tasks.length === 0 ? ( <Text className="text-center text-foreground text-lg"> Please add your first task... </Text> ) : ( tasks.map((task) => ( <Task key={task.id} task={task} onUpdate={handleTaskUpdate} /> )) )}</ScrollView>
Step 7: Update the Task Component to Propagate Changes
Section titled “Step 7: Update the Task Component to Propagate Changes”export interface Task { id: number; title: string; category: string; isChecked: boolean;}
export interface TaskProps { task: Task; onUpdate?: (task: Task) => void;}
export default function Task({ task: propTask }: TaskProps) {export default function Task({ task: propTask, onUpdate }: TaskProps) { const [task, setTask] = React.useState(propTask); const [showDialog, setShowDialog] = React.useState(false); const { title, category, isChecked } = task;
const handleSetChecked = () => { const nextChecked = !task.isChecked; setTask({ ...task, isChecked: nextChecked }); const updatedTask = { ...task, isChecked: !task.isChecked }; setTask(updatedTask); if (onUpdate) { onUpdate(updatedTask); } };
const handleTaskUpdate = (updatedTask: Task) => { setTask(updatedTask); if (onUpdate) { onUpdate(updatedTask); } };
Step 8: Improve the TaskDialog Component
Section titled “Step 8: Improve the TaskDialog Component”interface TaskDialogProps { task: Task; setTask: (task: Task) => void; setShowDialog: (showDialog: boolean) => void; showDialog: boolean; onSave?: () => void;}
export default function TaskDialog({ task, setTask, setShowDialog, showDialog, onSave,}: TaskDialogProps) { const isNewTask = task.title === "" && task.category === "";
// Rest of the component...
const handleSave = () => { const nextTask = { ...task, title: editedTitle, category: editedCategory, };
setTask(nextTask); if (onSave) { onSave(); } else { setShowDialog(false); } };
// Rest of the component...}
<View className="gap-4"> <Input defaultValue={title} value={editedTitle} placeholder="Task title" onChangeText={handleUpdateTitle} /> <Input defaultValue={category} value={editedCategory} placeholder="Category" onChangeText={handleUpdateCategory} /></View>
<DialogFooter> <DialogClose asChild> <Button variant="outline"> <Text>Cancel</Text> </Button> </DialogClose> <DialogClose asChild> <Button onPress={handleSave}> <Text>Save changes</Text> </Button> </DialogClose> <Button variant="outline" onPress={() => setShowDialog(false)}> <Text>Cancel</Text> </Button> <Button onPress={handleSave}> <Text>Save changes</Text> </Button></DialogFooter>
Step 9: Reset Form Fields in AddTask
Section titled “Step 9: Reset Form Fields in AddTask”export default function AddTask({ onAdd }: AddTaskProps) { const [showDialog, setShowDialog] = React.useState(false); const [title, setTitle] = React.useState(""); const [category, setCategory] = React.useState("");
// This function will be called when the user saves a new task const handleSave = () => { if (title.trim()) { // Call the onAdd function passed from the parent component onAdd(title, category);
// Reset the form fields and close dialog setTitle(""); setCategory(""); setShowDialog(false); } };
// Reset fields when dialog closes React.useEffect(() => { if (!showDialog) { setTitle(""); setCategory(""); } }, [showDialog]);
How it all works together
Section titled “How it all works together”- When the app starts, it loads tasks from
AsyncStorage
- While loading, it shows a “Loading tasks…” message
- If no tasks exist, it shows a prompt to add the first task
- When a task is added or updated:
- The component state is updated
- The change is propagated to the parent component
- The parent saves all tasks to
AsyncStorage
- When the app is reopened later, the saved tasks are loaded from storage
Testing Your Implementation
Section titled “Testing Your Implementation”After implementing these changes:
- Add a few tasks to your app
- Close the app completely
- Reopen the app - your tasks should still be there!
- Edit a task or mark it as complete - these changes should persist after closing and reopening the app
That’s it!
Section titled “That’s it!”Congratulations! Your app now has a complete task management system with persistent storage.
Once you have implemented AsyncStorage
successfully, your app now:
- Loads tasks from storage when it starts
- Saves tasks whenever they’re added or updated
- Shows a loading state while tasks are being fetched
- Displays a message when no tasks exist
- All changes are automatically saved to the user’s device!
Users can add, edit, and complete tasks, and all changes are automatically saved to their device.