CodingBowl

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

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.

Meow! AI Assistance Note

This post was created with the assistance of Gemini AI and ChatGPT.
It is shared for informational purposes only and is not intended to mislead, cause harm, or misrepresent facts. While efforts have been made to ensure accuracy, readers are encouraged to verify information independently. Portions of the content may not be entirely original.

image
Photo by Yibo Wei on Unsplash