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.

Terminal window
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):

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 AsyncStorage
const 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”
app/index.tsx
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();
}, []);

Add this code to your main file. It will load tasks from storage when the app starts:

app/index.tsx
// Save tasks to storage whenever they change
const 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”
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: 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”
app/index.tsx
<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”
app/components/Task.tsx
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);
}
};
app/components/TaskDialogue.tsx
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...
}
app/components/TaskDialogue.tsx
<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>
app/components/AddTask.tsx
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]);
  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.