Build a real-world mobile app using React Native Expo and Firebase. This beginner-friendly JavaScript guide covers secure registration, login, image uploads with metadata, and full Edit/Delete capabilities with security rules.
Introduction
In this guide, we build a "Photo Diary" application. You will learn how to manage user accounts, store image files in the cloud, and sync data in real-time. We will use vanilla JavaScript and React Native's StyleSheet for a clean, professional look.
1. Firebase Configuration (firebaseConfig.js)
This file connects your frontend to your Google Cloud backend.
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT.appspot.com",
appId: "YOUR_APP_ID"
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
2. App Gatekeeper (App.js)
We use a listener to check if a user is registered and logged in.
import React, { useState, useEffect } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from './firebaseConfig';
import AuthScreen from './AuthScreen';
import GalleryScreen from './GalleryScreen';
export default function App() {
const [user, setUser] = useState(null);
useEffect(() => {
return onAuthStateChanged(auth, (u) => setUser(u));
}, []);
return user ? <GalleryScreen /> : <AuthScreen />;
}
3. Authentication Screen (AuthScreen.js)
This handles both User Registration and Login.
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'firebase/auth';
import { auth } from './firebaseConfig';
export default function AuthScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isRegistering, setIsRegistering] = useState(false);
const handleAuth = async () => {
try {
if (isRegistering) {
await createUserWithEmailAndPassword(auth, email, password);
} else {
await signInWithEmailAndPassword(auth, email, password);
}
} catch (err) { Alert.alert("Error", err.message); }
};
return (
<View style={styles.container}>
<Text style={styles.title}>{isRegistering ? "Create Account" : "Welcome Back"}</Text>
<TextInput placeholder="Email" style={styles.input} onChangeText={setEmail} value={email} autoCapitalize="none" />
<TextInput placeholder="Password" style={styles.input} secureTextEntry onChangeText={setPassword} value={password} />
<TouchableOpacity style={styles.btn} onPress={handleAuth}>
<Text style={styles.btnText}>{isRegistering ? "Sign Up" : "Login"}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setIsRegistering(!isRegistering)}>
<Text style={styles.toggleText}>
{isRegistering ? "Already have an account? Login" : "New here? Register now"}
</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', padding: 25, backgroundColor: '#fff' },
title: { fontSize: 30, fontWeight: 'bold', marginBottom: 30 },
input: { borderBottomWidth: 1, borderColor: '#ccc', marginBottom: 20, padding: 10 },
btn: { backgroundColor: '#007AFF', padding: 15, borderRadius: 10, alignItems: 'center' },
btnText: { color: '#fff', fontWeight: 'bold' },
toggleText: { marginTop: 20, textAlign: 'center', color: '#007AFF' }
});
4. The Gallery CRUD (GalleryScreen.js)
This includes Picking, Uploading, Reading, Editing, and Deleting.
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, Image, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Modal, Alert } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
import { collection, addDoc, onSnapshot, query, orderBy, deleteDoc, doc, updateDoc } from 'firebase/firestore';
import { auth, storage, db } from './firebaseConfig';
export default function GalleryScreen() {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(false);
const [title, setTitle] = useState('');
const [desc, setDesc] = useState('');
const [imageUri, setImageUri] = useState(null);
const [editItem, setEditItem] = useState(null);
useEffect(() => {
const q = query(collection(db, "posts"), orderBy("createdAt", "desc"));
return onSnapshot(q, (snap) => setImages(snap.docs.map(d => ({ id: d.id, ...d.data() }))));
}, []);
const pickImage = async () => {
let res = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, quality: 0.5 });
if (!res.canceled) setImageUri(res.assets[0].uri);
};
const handleUpload = async () => {
if (!imageUri || !title) return Alert.alert("Error", "Required fields missing!");
setLoading(true);
try {
const blob = await (await fetch(imageUri)).blob();
const sRef = ref(storage, `pics/${Date.now()}`);
await uploadBytes(sRef, blob);
const url = await getDownloadURL(sRef);
await addDoc(collection(db, "posts"), {
url, title, desc, path: sRef.fullPath, uid: auth.currentUser.uid, createdAt: new Date()
});
setImageUri(null); setTitle(''); setDesc('');
} catch (e) { Alert.alert("Error", e.message); }
setLoading(false);
};
const handleDelete = async (id, path) => {
await deleteObject(ref(storage, path));
await deleteDoc(doc(db, "posts", id));
};
return (
<View style={styles.container}>
<TouchableOpacity onPress={() => auth.signOut()}><Text>Logout</Text></TouchableOpacity>
<View style={styles.addCard}>
<TouchableOpacity style={styles.pickBtn} onPress={pickImage}><Text>Pick Image</Text></TouchableOpacity>
<TextInput placeholder="Title" style={styles.input} value={title} onChangeText={setTitle} />
<TextInput placeholder="Description" style={styles.input} value={desc} onChangeText={setDesc} />
<TouchableOpacity style={styles.upBtn} onPress={handleUpload}>
{loading ? <ActivityIndicator color="#fff" /> : <Text style={{color:'#fff'}}>Post</Text>}
</TouchableOpacity>
</View>
<FlatList data={images} keyExtractor={item => item.id} renderItem={({ item }) => (
<View style={styles.card}>
<Image source={{ uri: item.url }} style={styles.img} />
<View style={{padding: 10}}>
<Text style={{fontWeight: 'bold'}}>{item.title}</Text>
<Text>{item.desc}</Text>
<TouchableOpacity onPress={() => setEditItem(item)}><Text style={{color: 'blue'}}>Edit</Text></TouchableOpacity>
<TouchableOpacity onPress={() => handleDelete(item.id, item.path)}><Text style={{color: 'red'}}>Delete</Text></TouchableOpacity>
</View>
</View>
)} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5', paddingTop: 50 },
addCard: { backgroundColor: '#fff', margin: 15, padding: 15, borderRadius: 10 },
input: { borderBottomWidth: 1, borderColor: '#eee', marginBottom: 10 },
pickBtn: { backgroundColor: '#eee', padding: 10, alignItems: 'center' },
upBtn: { backgroundColor: '#007AFF', padding: 12, alignItems: 'center' },
card: { backgroundColor: '#fff', margin: 15, borderRadius: 10, overflow: 'hidden' },
img: { width: '100%', height: 200 }
});
5. Firebase Security Rules
Deploy these to the Firebase Console to protect your free tier.
// Firestore Rules
match /posts/{postId} {
allow read: if request.auth != null;
allow create: if request.auth != null && request.resource.data.uid == request.auth.uid;
allow update, delete: if request.auth != null && request.auth.uid == resource.data.uid;
}
// Storage Rules
match /pics/{allPaths=**} {
allow read, write: if request.auth != null;
}
Conclusion
You have now built a fully authenticated app with a live cloud database and image storage. Using the StyleSheet method ensures your app remains high-performance and modern.