Storing data: AsyncStorage
Right now tasks live only in state and disappear when the app is closed. AsyncStorage is a simple key-string store on the device, the counterpart of localStorage in the browser. The difference is that all its methods are asynchronous and return promises.
Installing
Go back to the todo project and install the package:
npx expo install @react-native-async-storage/async-storageThe package is included in Expo Go, so you do not need to rebuild the app; restarting npx expo start is enough.
Writing and reading
| Method | What it does |
|---|---|
AsyncStorage.setItem(key, value) | Saves a string |
AsyncStorage.getItem(key) | Returns a string or null |
AsyncStorage.removeItem(key) | Removes a value |
Only strings are stored, so the task array is turned into JSON. It is convenient to put this into a separate file:
// lib/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { Task } from '@/components/TaskItem';
const KEY = 'tasks';
export async function loadTasks(): Promise<Task[]> {
const json = await AsyncStorage.getItem(KEY);
return json ? JSON.parse(json) : [];
}
export async function saveTasks(tasks: Task[]) {
await AsyncStorage.setItem(KEY, JSON.stringify(tasks));
}Create a lib folder in the project root. loadTasks returns an empty array if nothing has been saved yet.
Connecting it to the screen
On the main screen, load the tasks once when it opens and save them on every change:
// app/index.tsx
import { loadTasks, saveTasks } from '@/lib/storage';
const [tasks, setTasks] = useState<Task[]>([]);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
loadTasks().then((saved) => {
setTasks(saved);
setLoaded(true);
});
}, []);
useEffect(() => {
if (loaded) saveTasks(tasks);
}, [tasks, loaded]);Add a couple of tasks, close the app completely and open it again: the tasks are still there. The loaded flag allows saving only after loading.
Where the data lives
AsyncStorage keeps data in a regular app file without encryption. Uninstalling the app erases it. It suits settings, drafts, caches and lists like ours. For large amounts of data and complex queries, use the expo-sqlite database.
Common mistake
Saving without the loaded flag:
// app/index.tsx
// Wrong:
useEffect(() => {
saveTasks(tasks);
}, [tasks]);On the first render tasks is an empty array, and the effect writes it to storage before loadTasks manages to read the old tasks. After every restart the list is empty. Save only after the data has been loaded.