Welcome back, Developer! 👋
In our last post — From Zero to App: Your First React Native App with Expo — we built a simple but functional app where you could manage users, add profile pictures, and handle some basic data.
Now it’s time to take things up a notch 😎
This time, we’re diving into data persistence — making sure your app remembers things like user preferences, login sessions, and settings even after it’s been closed.
We’ll explore some of the main storage options available in the React Native and Expo ecosystem:
AsyncStorage
— the go-to for simple key-value storage.Expo SecureStore
— perfect for securely storing sensitive data like tokens.
You’ll not only know when to use each one, but you’ll also see them in action — all within a real-world student-focused app. Sold?
Table of contents
Open Table of contents
Intro
We’re continuing with our User Management app but giving it a more specific purpose — a simple mobile companion for students to manage their daily life on campus. Think of it as a place where students can log in, update their profile, switch between dark and light themes, and (eventually) keep track of things like tasks and study sessions.
In this post, we’ll enhance the app by making sure it remembers your login, preferences, and other settings — even after you close and reopen it.
Grab your favorite beverage and let’s dive in!
Setting things up
Before we get into the implementation, we’ll need to set up the necessary dependencies. Open your terminal and install the required packages:
npx create-expo-app@latest student-mate --template default
cd student-mate
npx expo install @react-native-async-storage/async-storage expo-secure-store
Boom 💥 you’re ready.
Time to build
In the next sections, we are using different scenarios where we store and read data to illustrate the implementation of the storage solutions.
TL;DR
- Use
AsyncStorage
for non-sensitive data (e.g., theme, last seen screen) - Use
SecureStore
for sensitive stuff (e.g., tokens, emails, PINs)
AsyncStorage
Let’s persist the user’s login using AsyncStorage
. On app/(tabs)/index.tsx
, replace the existing code with the following:
import React, { useEffect, useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
useColorScheme,
Alert,
} from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const theme = isDark ? darkTheme : lightTheme;
useEffect(() => {
const checkLoginStatus = async () => {
const user = await AsyncStorage.getItem("user");
if (user) {
setIsLoggedIn(true);
setUsername(user);
}
};
checkLoginStatus();
}, []);
const handleLogin = async () => {
if (username && password) {
await AsyncStorage.setItem("user", username);
setIsLoggedIn(true);
} else {
Alert.alert("Username and Password required!");
}
};
const handleLogout = async () => {
await AsyncStorage.removeItem("user");
setIsLoggedIn(false);
setUsername("");
setPassword("");
};
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
<Text style={[styles.title, { color: theme.text }]}>Student Mate</Text>
{isLoggedIn ? (
<View style={styles.innerContainer}>
<Text style={[styles.welcomeText, { color: theme.text }]}>
Welcome, {username}!
</Text>
<TouchableOpacity
style={[styles.button, { backgroundColor: theme.primary }]}
onPress={handleLogout}
>
<Text style={styles.buttonText}>Logout</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.innerContainer}>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.inputBackground,
color: theme.text,
borderColor: theme.border,
},
]}
placeholder="Username"
placeholderTextColor={theme.placeholder}
value={username}
onChangeText={setUsername}
/>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.inputBackground,
color: theme.text,
borderColor: theme.border,
},
]}
placeholder="Password"
placeholderTextColor={theme.placeholder}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
style={[styles.button, { backgroundColor: theme.primary }]}
onPress={handleLogin}
>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
</View>
)}
</View>
);
}
const lightTheme = {
background: "#FFFFFF",
text: "#222222",
primary: "purple",
inputBackground: "#F2F2F7",
border: "#D1D1D6",
placeholder: "#999999",
};
const darkTheme = {
background: "#121212",
text: "#FFFFFF",
primary: "purple",
inputBackground: "#1E1E1E",
border: "#3A3A3C",
placeholder: "#AAAAAA",
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 24,
},
title: {
fontSize: 28,
fontWeight: "bold",
marginBottom: 32,
},
innerContainer: {
alignItems: "center",
width: "100%",
},
welcomeText: {
fontSize: 22,
fontWeight: "600",
marginBottom: 20,
},
input: {
width: "100%",
padding: 14,
borderWidth: 1,
borderRadius: 8,
marginBottom: 16,
fontSize: 16,
},
button: {
width: "100%",
padding: 14,
borderRadius: 8,
alignItems: "center",
},
buttonText: {
color: "#FFFFFF",
fontWeight: "600",
fontSize: 16,
},
});
How it works
Let’s walk through the core logic of the login feature, Developer. The focus is on how AsyncStorage helps us persist user sessions across app restarts.
- Check if user is already logged in when the app loads, we use the useEffect hook to check if a user has already logged in before:
useEffect(() => {
const checkLoginStatus = async () => {
const user = await AsyncStorage.getItem("user");
if (user) {
setIsLoggedIn(true);
setUsername(user);
}
};
checkLoginStatus();
}, []);
Here’s what’s happening:
AsyncStorage.getItem("user")
retrieves the stored username, if it exists.- If a value is returned, we update the local state to reflect that the user is logged in. This means the user stays logged in even after closing or restarting the app — no need to log in again until they explicitly log out.
- Login and save credentials when the user submits the login form, we verify the credentials and save the username:
const handleLogin = async () => {
if (username && password) {
await AsyncStorage.setItem("user", username);
// update local state
setIsLoggedIn(true);
} else {
Alert.alert("Username and Password are required!");
}
};
Here’s what’s happening:
- If the username/password is informed, we store the username in AsyncStorage. This makes the session persistent.
- We then flip the state to show the logged-in view.
AsyncStorage.setItem("user", username)
stores data as a key-value pair. It’s string-based, so if you’re storing complex objects, remember toJSON.stringify()
them.
- Logout and clear session. Logging out clears the session from both memory and storage:
const handleLogout = async () => {
// remove user data from async storage
await AsyncStorage.removeItem("user");
// update local state
setIsLoggedIn(false);
setUsername("");
setPassword("");
};
Here’s what’s happening:
- Remove the user key from AsyncStorage.
- Reset all local state variables to ensure a clean slate. The next time the app opens, the login screen will be shown again.
So why use AsyncStorage? AsyncStorage is React Native’s simple key-value storage system. It’s perfect for:
- Storing small amounts of persistent data (like user tokens or settings).
- Keeping users logged in between sessions.
- Providing a lightweight alternative to more complex storage solutions (like SQLite or Realm) when all you need is basic persistence.
AsyncStorage isn’t encrypted — don’t store tokens or anything private.
Expo SecureStore
Now let’s see how we can securely store user data in our Student Mate app. Here we use Expo SecureStore
to save the password (just for demonstration purposes — don’t do this in production!)
Let’s adjust the previous code by adding the logic to store the user password:
import React, { useEffect, useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
useColorScheme,
Alert,
} from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as SecureStore from "expo-secure-store";
export default function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const theme = isDark ? darkTheme : lightTheme;
useEffect(() => {
const checkLoginStatus = async () => {
const user = await AsyncStorage.getItem("user");
if (user) {
const savedPassword = await SecureStore.getItemAsync("password");
if (savedPassword) {
setUsername(user);
setPassword(savedPassword);
setIsLoggedIn(true);
}
}
};
checkLoginStatus();
}, []);
const handleLogin = async () => {
if (username && password) {
await AsyncStorage.setItem("user", username);
await SecureStore.setItemAsync("password", password);
setIsLoggedIn(true);
} else {
Alert.alert("Username and Password required!");
}
};
const handleLogout = async () => {
await AsyncStorage.removeItem("user");
await SecureStore.deleteItemAsync("password");
setIsLoggedIn(false);
setUsername("");
setPassword("");
};
return (
<View style={[styles.container, { backgroundColor: theme.background }]}>
<Text style={[styles.title, { color: theme.text }]}>Student Mate</Text>
{isLoggedIn ? (
<View style={styles.innerContainer}>
<Text style={[styles.welcomeText, { color: theme.text }]}>
Welcome, {username}!
</Text>
<TouchableOpacity
style={[styles.button, { backgroundColor: theme.primary }]}
onPress={handleLogout}
>
<Text style={styles.buttonText}>Logout</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.innerContainer}>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.inputBackground,
color: theme.text,
borderColor: theme.border,
},
]}
placeholder="Username"
placeholderTextColor={theme.placeholder}
value={username}
onChangeText={setUsername}
/>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.inputBackground,
color: theme.text,
borderColor: theme.border,
},
]}
placeholder="Password"
placeholderTextColor={theme.placeholder}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity
style={[styles.button, { backgroundColor: theme.primary }]}
onPress={handleLogin}
>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
</View>
)}
</View>
);
}
const lightTheme = {
background: "#FFFFFF",
text: "#222222",
primary: "purple",
inputBackground: "#F2F2F7",
border: "#D1D1D6",
placeholder: "#999999",
};
const darkTheme = {
background: "#121212",
text: "#FFFFFF",
primary: "purple",
inputBackground: "#1E1E1E",
border: "#3A3A3C",
placeholder: "#AAAAAA",
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 24,
},
title: {
fontSize: 28,
fontWeight: "bold",
marginBottom: 32,
},
innerContainer: {
alignItems: "center",
width: "100%",
},
welcomeText: {
fontSize: 22,
fontWeight: "600",
marginBottom: 20,
},
input: {
width: "100%",
padding: 14,
borderWidth: 1,
borderRadius: 8,
marginBottom: 16,
fontSize: 16,
},
button: {
width: "100%",
padding: 14,
borderRadius: 8,
alignItems: "center",
},
buttonText: {
color: "#FFFFFF",
fontWeight: "600",
fontSize: 16,
},
});
Behind the scenes
- Auto-login with stored credentials
useEffect(() => {
const checkLoginStatus = async () => {
const user = await AsyncStorage.getItem("user");
if (user) {
const savedPassword = await SecureStore.getItemAsync("password");
if (savedPassword) {
setUsername(user);
setPassword(savedPassword);
setIsLoggedIn(true);
}
}
};
checkLoginStatus();
}, []);
Here’s what’s happening:
- When the app loads, it checks if a user is stored in AsyncStorage.
- If found, it then tries to get the corresponding password from SecureStore.
- If both are available, it logs the user in automatically.
Passwords should never be stored in plain text — SecureStore keeps them encrypted using native device-level protection (Keychain on iOS, Keystore on Android).
- Storing Credentials on Login
const handleLogin = async () => {
if (username && password) {
await AsyncStorage.setItem("user", username);
await SecureStore.setItemAsync("password", password);
setIsLoggedIn(true);
} else {
Alert.alert("Username and Password required!");
}
};
Here’s what’s happening:
- If the user fills in both username and password, we store:
- username using
AsyncStorage
- password using
SecureStore
- username using
- Clearing Storage on Logout
const handleLogout = async () => {
await AsyncStorage.removeItem("user");
await SecureStore.deleteItemAsync("password");
setIsLoggedIn(false);
setUsername("");
setPassword("");
};
Here’s what’s happening:
- When the user logs out:
- We wipe the stored user from AsyncStorage
- We also delete the encrypted password from SecureStore
- This guarantees that the session is fully cleared and sensitive data is not lingering on the device.
So why use Expo SecureStore? Expo SecureStore is a simple, secure storage solution for sensitive data in mobile apps. It’s ideal for:
- Storing sensitive information like authentication tokens, passwords, or encryption keys.
- Ensuring data is encrypted and stored safely on the device, even if the app is closed or the device is restarted.
- Providing a straightforward solution for secure data storage without needing to implement complex encryption or storage strategies.
SecureStore isn’t ideal for large data or frequent writes/reads.
You Did It — Congrats
Congrats! You’ve just learned how to persist data in your Expo app using AsyncStorage and SecureStore. That means your app can now remember things — even after it’s closed. Pretty cool, right?
These are the kinds of patterns that turn good apps into great ones. And more importantly, you’ve built up your confidence to make smart architectural decisions when it comes to data storage on mobile devices.
But this is just the beginning. Keep experimenting, keep refactoring, and keep building!
Thanks for reading, and I can’t wait to see what you build next. If you found this helpful, stick around and stay tuned for more tutorials.
See you in the next one, Developer. Enjoy the journey and have fun! 💙