The Ultimate 2026 Guide to Expo & Firebase: Auth, Image Uploads, and Full CRUD

Published on 11 Jan 2026 Tech Mobile App
image
Photo by Elimende Inagella on Unsplash
Disclaimer: This post was created with the assistance of Gemini and/or ChatGPT for informational purposes. While given a quick look over, readers are encouraged to verify key facts independently.

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.

Comments