Skip to content

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: Import AsyncStorage and Define a Storage Key

Section titled “Step 1: Import AsyncStorage and Define a Storage Key”

In your main file (app/index.tsx):

app/index.tsx
import * as React from 'react';
import { ScrollView, View } from 'react-native';
import { Text } from '@/components/ui/text';
import { Task } from '@/components/Task';
import { AddTask } from '@/components/AddTask';
import AsyncStorage from "@react-native-async-storage/async-storage";
// Key for storing tasks in AsyncStorage
const TASKS_STORAGE_KEY = "hallpass_tasks";
interface ITask {
id: number;
title: string;
category: string;
isChecked: boolean;
}

Step 2: Load Tasks from Storage on App Start

Section titled “Step 2: Load Tasks from Storage on App Start”
app/index.tsx
export default function HomeScreen() {
const initialTasks: ITask[] = [
{
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,
},
];
const [tasks, setTasks] = React.useState<ITask[]>(initialTasks);
const [tasks, setTasks] = React.useState<ITask[]>([]);
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();
}, []);
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 }]);
};
// More code here...

Add this code to your main app/index.tsx file, inside the HomeScreen component.

It will be used shortly to load tasks from storage when the app starts:

app/index.tsx
export default function HomeScreen() {
const [tasks, setTasks] = React.useState<ITask[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
// Load tasks from storage when app starts
React.useEffect(() => {
14 collapsed lines
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();
}, []);
// Save tasks to storage whenever they change
const saveTasks = async (updatedTasks: ITask[]) => {
try {
await AsyncStorage.setItem(TASKS_STORAGE_KEY, JSON.stringify(updatedTasks));
} catch (error) {
console.error('Failed to save tasks:', error);
}
};
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 }]);
};

Step 4: Update Task Handlers to Save Changes

Section titled “Step 4: Update Task Handlers to Save Changes”
app/index.tsx
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: ITask) => {
const updatedTasks = tasks.map((task) =>
task.id === updatedTask.id ? updatedTask : task
);
setTasks(updatedTasks);
saveTasks(updatedTasks);
};

Step 5: Update the UI to Handle Loading State

Section titled “Step 5: Update the UI to Handle Loading State”

Step 6: Update the Task Component to Propagate Changes

Section titled “Step 6: Update the Task Component to Propagate Changes”
app/components/Task.tsx
export interface TaskProps {
task: Task;
onUpdate?: (task: ITask) => void;
}
function Task({ task: initialTask, onUpdate }: TaskProps) {
const [task, setTask] = React.useState(initialTask);
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);
}
};

Open components/TaskDialogue.tsx and update the following code on your Input and DialogClose JSX components:

app/components/TaskDialogue.tsx
<View className="gap-4">
<Input
defaultValue={task.title}
value={editedTitle}
placeholder="Task title"
onChangeText={handleUpdateTitle}
/>
<Input
defaultValue={task.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
className="border-brand-primary flex-1 rounded-3xl border bg-transparent"
onPress={() => setShowDialog(false)}>
<Text className="text-brand-primary">Cancel</Text>
</Button>
<Button className="bg-brand-primary flex-1w-1/2 rounded-3xl" onPress={handleSave}>
<Text>Save changes</Text>
</Button>
</DialogFooter>
  1. When the app starts, it loads tasks from AsyncStorage
  2. While loading, it shows a “Loading tasks…” message
  3. If no tasks exist, it shows a prompt to add the first task
  4. 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

After implementing these changes:

  1. Add a few tasks to your app
  2. Close the app completely
  3. Reopen the app - your tasks should still be there!
  4. Edit a task or mark it as complete - these changes should persist after closing and reopening the app

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.