Implement local data persistence using AsyncStorage to save user photos, albums, and app preferences.
Creative apps must remember user data between sessions:
Without storage, users lose everything when they close the app.
Install AsyncStorage:
expo install @react-native-async-storage/async-storage
import AsyncStorage from '@react-native-async-storage/async-storage';
// Save data
const saveData = async (key, value) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving data:', error);
}
};
// Load data
const loadData = async (key) => {
try {
const value = await AsyncStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Error loading data:', error);
return null;
}
};
// Remove data
const removeData = async (key) => {
try {
await AsyncStorage.removeItem(key);
} catch (error) {
console.error('Error removing data:', error);
}
};
Create a photo storage system for your Creative Studio:
import AsyncStorage from '@react-native-async-storage/async-storage';
const PHOTOS_KEY = 'creative_studio_photos';
const ALBUMS_KEY = 'creative_studio_albums';
class PhotoStorage {
// Save a new photo
static async savePhoto(photo) {
try {
const existingPhotos = await this.loadPhotos();
const newPhoto = {
id: Date.now().toString(),
uri: photo.uri,
width: photo.width,
height: photo.height,
createdAt: new Date().toISOString(),
albums: [], // Which albums contain this photo
...photo
};
const updatedPhotos = [...existingPhotos, newPhoto];
await AsyncStorage.setItem(PHOTOS_KEY, JSON.stringify(updatedPhotos));
return newPhoto;
} catch (error) {
console.error('Error saving photo:', error);
throw error;
}
}
// Load all photos
static async loadPhotos() {
try {
const photosJson = await AsyncStorage.getItem(PHOTOS_KEY);
return photosJson ? JSON.parse(photosJson) : [];
} catch (error) {
console.error('Error loading photos:', error);
return [];
}
}
// Delete a photo
static async deletePhoto(photoId) {
try {
const photos = await this.loadPhotos();
const updatedPhotos = photos.filter(photo => photo.id !== photoId);
await AsyncStorage.setItem(PHOTOS_KEY, JSON.stringify(updatedPhotos));
} catch (error) {
console.error('Error deleting photo:', error);
throw error;
}
}
// Update photo data (after editing)
static async updatePhoto(photoId, updates) {
try {
const photos = await this.loadPhotos();
const updatedPhotos = photos.map(photo =>
photo.id === photoId ? { ...photo, ...updates } : photo
);
await AsyncStorage.setItem(PHOTOS_KEY, JSON.stringify(updatedPhotos));
return updatedPhotos.find(photo => photo.id === photoId);
} catch (error) {
console.error('Error updating photo:', error);
throw error;
}
}
}
Create an album system:
class AlbumStorage {
// Create new album
static async createAlbum(name, description = '') {
try {
const albums = await this.loadAlbums();
const newAlbum = {
id: Date.now().toString(),
name,
description,
photoIds: [],
createdAt: new Date().toISOString(),
coverPhotoId: null,
};
const updatedAlbums = [...albums, newAlbum];
await AsyncStorage.setItem(ALBUMS_KEY, JSON.stringify(updatedAlbums));
return newAlbum;
} catch (error) {
console.error('Error creating album:', error);
throw error;
}
}
// Load all albums
static async loadAlbums() {
try {
const albumsJson = await AsyncStorage.getItem(ALBUMS_KEY);
return albumsJson ? JSON.parse(albumsJson) : [];
} catch (error) {
console.error('Error loading albums:', error);
return [];
}
}
// Add photo to album
static async addPhotoToAlbum(albumId, photoId) {
try {
const albums = await this.loadAlbums();
const updatedAlbums = albums.map(album => {
if (album.id === albumId) {
const photoIds = [...album.photoIds];
if (!photoIds.includes(photoId)) {
photoIds.push(photoId);
return {
...album,
photoIds,
coverPhotoId: album.coverPhotoId || photoId
};
}
}
return album;
});
await AsyncStorage.setItem(ALBUMS_KEY, JSON.stringify(updatedAlbums));
} catch (error) {
console.error('Error adding photo to album:', error);
throw error;
}
}
}
Store user settings and preferences:
const PREFERENCES_KEY = 'creative_studio_preferences';
class PreferencesStorage {
// Default preferences
static defaultPreferences = {
theme: 'dark',
autoSavePhotos: true,
imageQuality: 0.8,
showTutorials: true,
defaultAlbum: 'Camera Roll',
lastUsedFilter: null,
};
// Load preferences
static async loadPreferences() {
try {
const prefsJson = await AsyncStorage.getItem(PREFERENCES_KEY);
const savedPrefs = prefsJson ? JSON.parse(prefsJson) : {};
return { ...this.defaultPreferences, ...savedPrefs };
} catch (error) {
console.error('Error loading preferences:', error);
return this.defaultPreferences;
}
}
// Save preferences
static async savePreferences(preferences) {
try {
await AsyncStorage.setItem(PREFERENCES_KEY, JSON.stringify(preferences));
} catch (error) {
console.error('Error saving preferences:', error);
throw error;
}
}
// Update single preference
static async updatePreference(key, value) {
try {
const currentPrefs = await this.loadPreferences();
const updatedPrefs = { ...currentPrefs, [key]: value };
await this.savePreferences(updatedPrefs);
return updatedPrefs;
} catch (error) {
console.error('Error updating preference:', error);
throw error;
}
}
}
Create a custom hook for photo management:
import { useState, useEffect } from 'react';
function usePhotoStorage() {
const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true);
// Load photos when hook initializes
useEffect(() => {
loadPhotos();
}, []);
const loadPhotos = async () => {
try {
setLoading(true);
const loadedPhotos = await PhotoStorage.loadPhotos();
setPhotos(loadedPhotos);
} catch (error) {
console.error('Error loading photos:', error);
} finally {
setLoading(false);
}
};
const addPhoto = async (photo) => {
try {
const savedPhoto = await PhotoStorage.savePhoto(photo);
setPhotos(prevPhotos => [...prevPhotos, savedPhoto]);
return savedPhoto;
} catch (error) {
console.error('Error adding photo:', error);
throw error;
}
};
const deletePhoto = async (photoId) => {
try {
await PhotoStorage.deletePhoto(photoId);
setPhotos(prevPhotos =>
prevPhotos.filter(photo => photo.id !== photoId)
);
} catch (error) {
console.error('Error deleting photo:', error);
throw error;
}
};
return {
photos,
loading,
addPhoto,
deletePhoto,
refreshPhotos: loadPhotos,
};
}
Use storage in your Creative Studio components:
import React from 'react';
import { View, FlatList, Text, StyleSheet } from 'react-native';
function GalleryScreen() {
const { photos, loading, deletePhoto } = usePhotoStorage();
if (loading) {
return (
<View style={styles.centered}>
<Text style={styles.loadingText}>Loading your photos...</Text>
</View>
);
}
const renderPhoto = ({ item }) => (
<TouchableOpacity
style={styles.photoItem}
onLongPress={() => deletePhoto(item.id)}
>
<Image source={{ uri: item.uri }} style={styles.thumbnail} />
<Text style={styles.photoDate}>
{new Date(item.createdAt).toLocaleDateString()}
</Text>
</TouchableOpacity>
);
return (
<View style={styles.container}>
<Text style={styles.title}>Your Photos ({photos.length})</Text>
<FlatList
data={photos}
renderItem={renderPhoto}
numColumns={3}
keyExtractor={(item) => item.id}
/>
</View>
);
}
const safeLoadData = async (key, fallback = null) => {
try {
const data = await AsyncStorage.getItem(key);
return data ? JSON.parse(data) : fallback;
} catch (error) {
console.error(`Error loading ${key}:`, error);
return fallback;
}
};
const saveBatch = async (items) => {
const keyValuePairs = items.map(({ key, value }) => [
key,
JSON.stringify(value)
]);
await AsyncStorage.multiSet(keyValuePairs);
};
For professional-quality apps, consider implementing offline-first capabilities that work seamlessly without internet connectivity.
Modern users expect apps to work everywhere:
For complex apps requiring robust offline functionality:
// services/OfflineDatabase.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SQLite from 'expo-sqlite';
export interface SyncableRecord {
id: string;
localId?: string;
createdAt: Date;
updatedAt: Date;
syncStatus: 'pending' | 'synced' | 'conflict';
version: number;
data: Record<string, any>;
}
class AdvancedStorage {
private static instance: AdvancedStorage;
private db: SQLite.WebSQLDatabase | null = null;
static getInstance(): AdvancedStorage {
if (!AdvancedStorage.instance) {
AdvancedStorage.instance = new AdvancedStorage();
}
return AdvancedStorage.instance;
}
async initialize(): Promise<void> {
this.db = SQLite.openDatabase('CreativeStudio.db');
await this.createTables();
}
private createTables(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not available'));
return;
}
this.db.transaction(
(tx) => {
// Photos table with sync capabilities
tx.executeSql(`
CREATE TABLE IF NOT EXISTS photos (
local_id TEXT PRIMARY KEY,
server_id TEXT,
uri TEXT NOT NULL,
width INTEGER,
height INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
sync_status TEXT NOT NULL DEFAULT 'pending',
version INTEGER NOT NULL DEFAULT 1
);
`);
// Albums table
tx.executeSql(`
CREATE TABLE IF NOT EXISTS albums (
local_id TEXT PRIMARY KEY,
server_id TEXT,
name TEXT NOT NULL,
description TEXT,
photo_ids TEXT,
cover_photo_id TEXT,
created_at INTEGER NOT NULL,
sync_status TEXT NOT NULL DEFAULT 'pending'
);
`);
},
(error) => reject(error),
() => resolve()
);
});
}
async savePhotoOffline(photo: any): Promise<string> {
if (!this.db) throw new Error('Database not initialized');
const localId = `photo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const now = Date.now();
return new Promise((resolve, reject) => {
this.db!.transaction(
(tx) => {
tx.executeSql(
'INSERT INTO photos (local_id, uri, width, height, created_at, updated_at, sync_status, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[localId, photo.uri, photo.width, photo.height, now, now, 'pending', 1],
() => resolve(localId),
(_, error) => reject(error)
);
},
(error) => reject(error)
);
});
}
async loadPhotosOffline(): Promise<any[]> {
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
this.db!.transaction(
(tx) => {
tx.executeSql(
'SELECT * FROM photos ORDER BY updated_at DESC',
[],
(_, result) => {
const photos = [];
for (let i = 0; i < result.rows.length; i++) {
const row = result.rows.item(i);
photos.push({
id: row.server_id || row.local_id,
localId: row.local_id,
uri: row.uri,
width: row.width,
height: row.height,
createdAt: new Date(row.created_at),
syncStatus: row.sync_status,
});
}
resolve(photos);
},
(_, error) => reject(error)
);
},
(error) => reject(error)
);
});
}
}
export default AdvancedStorage;
// services/OfflineManager.ts
import NetInfo from '@react-native-community/netinfo';
class OfflineManager {
private isOnline = true;
private pendingSync: any[] = [];
constructor() {
this.initializeNetworkMonitoring();
}
private initializeNetworkMonitoring() {
NetInfo.addEventListener(state => {
this.isOnline = state.isConnected || false;
if (this.isOnline && this.pendingSync.length > 0) {
this.syncPendingData();
}
});
}
async savePhoto(photo: any): Promise<string> {
// Always save locally first
const localId = await AdvancedStorage.getInstance().savePhotoOffline(photo);
if (this.isOnline) {
// Try to sync immediately
try {
await this.syncPhotoToServer(localId, photo);
} catch (error) {
console.log('Sync failed, will retry when online');
this.addToPendingSync({ type: 'photo', localId, data: photo });
}
} else {
// Add to sync queue for later
this.addToPendingSync({ type: 'photo', localId, data: photo });
}
return localId;
}
private async syncPhotoToServer(localId: string, photo: any): Promise<void> {
// Implementation would sync to your backend
console.log('Syncing photo to server:', localId);
// After successful sync, mark as synced in local database
}
private addToPendingSync(item: any): void {
this.pendingSync.push(item);
}
private async syncPendingData(): Promise<void> {
console.log(`Syncing ${this.pendingSync.length} pending items...`);
for (const item of this.pendingSync) {
try {
if (item.type === 'photo') {
await this.syncPhotoToServer(item.localId, item.data);
}
// Remove from pending after successful sync
this.pendingSync = this.pendingSync.filter(pending => pending !== item);
} catch (error) {
console.error('Sync failed for item:', item, error);
}
}
}
getOfflineStatus(): { isOnline: boolean; pendingItems: number } {
return {
isOnline: this.isOnline,
pendingItems: this.pendingSync.length,
};
}
}
export default new OfflineManager();
// hooks/useOfflinePhotos.ts
import { useState, useEffect } from 'react';
import OfflineManager from '../services/OfflineManager';
import AdvancedStorage from '../services/AdvancedStorage';
function useOfflinePhotos() {
const [photos, setPhotos] = useState([]);
const [loading, setLoading] = useState(true);
const [offlineStatus, setOfflineStatus] = useState(OfflineManager.getOfflineStatus());
useEffect(() => {
initializeStorage();
// Monitor offline status
const statusInterval = setInterval(() => {
setOfflineStatus(OfflineManager.getOfflineStatus());
}, 1000);
return () => clearInterval(statusInterval);
}, []);
const initializeStorage = async () => {
try {
await AdvancedStorage.getInstance().initialize();
await loadPhotos();
} catch (error) {
console.error('Storage initialization error:', error);
}
};
const loadPhotos = async () => {
try {
setLoading(true);
const loadedPhotos = await AdvancedStorage.getInstance().loadPhotosOffline();
setPhotos(loadedPhotos);
} catch (error) {
console.error('Error loading photos:', error);
} finally {
setLoading(false);
}
};
const addPhoto = async (photo: any) => {
try {
const localId = await OfflineManager.savePhoto(photo);
const newPhoto = {
id: localId,
localId,
...photo,
createdAt: new Date(),
syncStatus: offlineStatus.isOnline ? 'synced' : 'pending',
};
setPhotos(prevPhotos => [newPhoto, ...prevPhotos]);
return newPhoto;
} catch (error) {
console.error('Error adding photo:', error);
throw error;
}
};
return {
photos,
loading,
addPhoto,
refreshPhotos: loadPhotos,
offlineStatus,
};
}
export default useOfflinePhotos;
💡 Offline Tip: Offline-first architecture ensures your Creative Studio works perfectly whether users are on WiFi, cellular, or completely offline. Photos are always saved locally first, then synced to the cloud when connectivity is available.
You've mastered:
Your Creative Studio can now remember user data between app sessions and work reliably in any network condition, creating a truly professional user experience.
Next: You'll learn performance optimization techniques to make your app lightning-fast.