Persistence and device APIs
Choose between AsyncStorage, MMKV and SecureStore, then use camera, location, notifications and the file system with permissions handled at the point of use.
Choosing a storage layer
| Store | Performance | Use for |
|---|---|---|
AsyncStorage | Async, JSON strings | Small amounts of non-sensitive data |
react-native-mmkv | Synchronous, very fast | Frequently read preferences and cached state |
expo-secure-store | Keychain and Keystore | Tokens, refresh tokens, secrets |
react-native-fs | File APIs | Images, downloaded documents, exports |
| SQLite or WatermelonDB | Relational | Large structured datasets with queries |
import { MMKV } from 'react-native-mmkv';
import * as SecureStore from 'expo-secure-store';
const storage = new MMKV({ id: 'app-cache' });
export const settings = {
get theme() {
return storage.getString('theme') ?? 'system';
},
setTheme(value: 'light' | 'dark' | 'system') {
storage.set('theme', value);
},
};
export async function saveSession(token: string, refresh: string) {
await SecureStore.setItemAsync('access_token', token, {
keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
});
await SecureStore.setItemAsync('refresh_token', refresh);
}
export async function clearSession() {
await SecureStore.deleteItemAsync('access_token');
await SecureStore.deleteItemAsync('refresh_token');
}MMKV is synchronous, which is its main advantage: reading a stored theme during the first render needs no loading state and causes no flash of the wrong colour.
Camera, location and files
import { CameraView, useCameraPermissions } from 'expo-camera';
import * as Location from 'expo-location';
import * as FileSystem from 'expo-file-system';
export function Scanner({ onScanned }: { onScanned: (data: string) => void }) {
const [permission, requestPermission] = useCameraPermissions();
if (!permission) return null; // still loading
if (!permission.granted) {
return (
<View>
<Text>Camera access is needed to scan codes.</Text>
{permission.canAskAgain ? (
<Button title="Allow camera" onPress={requestPermission} />
) : (
<Button title="Open settings" onPress={() => Linking.openSettings()} />
)}
</View>
);
}
return (
<CameraView
style={{ flex: 1 }}
barcodeScannerSettings={{ barcodeTypes: ['qr', 'ean13'] }}
onBarcodeScanned={({ data }) => onScanned(data)}
/>
);
}
export async function exportJson(name: string, payload: unknown) {
const dir = FileSystem.documentDirectory ?? FileSystem.cacheDirectory!;
const uri = dir + name + '.json';
await FileSystem.writeAsStringAsync(uri, JSON.stringify(payload));
return uri;
}- Denied-forever is a different state from denied: offer a link to system settings instead of asking again, because the prompt will not appear.
- Request permissions at the moment the feature is used, with a sentence explaining why, not on first launch.
- Write user documents to
documentDirectoryand regenerable files tocacheDirectory, which the system may clear. - Stop location updates when the screen loses focus, or you drain the battery and appear in the background-usage review.
Notifications and background work
import * as Notifications from 'expo-notifications';
export async function registerForPush(): Promise<string | null> {
const existing = await Notifications.getPermissionsAsync();
let status = existing.status;
if (status !== 'granted') {
status = (await Notifications.requestPermissionsAsync()).status;
}
if (status !== 'granted') return null;
const token = (await Notifications.getExpoPushTokenAsync()).data;
await request('/devices/push-token', { method: 'POST', body: JSON.stringify({ token }) });
return token;
}
Notifications.addNotificationResponseReceivedListener((response) => {
const deepLink = response.notification.request.content.data?.url as string | undefined;
if (deepLink) router.push(deepLink);
});⚠️
Android 13 and later requires the runtime
POST_NOTIFICATIONS permission and iOS shows the system prompt only once. Ask after the user has done something that makes notifications obviously useful, or you permanently lose the chance.FAQ
Is AsyncStorage safe for a login token?
No. It is unencrypted and readable from a device backup. Use SecureStore or the Keychain for anything that grants access, and keep AsyncStorage for preferences and cached content.
How do I debug a permission that is always denied?
The grant persists until the app is uninstalled or the user changes it in settings. Uninstall and reinstall, or reset the permission for the app in the device settings, before testing the prompt path again.
Related
Networking, data fetching and offline support Native modules, TurboModules and the New Architecture
Last refreshed 2026-09-18.