Firebase Firestore in React: Storing and Retrieving User Data

Building upon Firebase Authentication to Create a Complete Data-Driven Application In our previous tutorial, we successfully implemented Firebase Authentication in a React application. Now that users can sign up and log in, the next logical step is to allow them to store and manage their data. In this tutorial, we'll integrate Firebase Firestore (Firebase's NoSQL database) to create a simple blog post system where authenticated users can create, store, and retrieve their blog posts. Table of Contents What We'll Build Understanding Firebase Firestore Prerequisites Step 1: Enable Firestore in Firebase Console Step 2: Update Firebase Configuration Step 3: Understanding Firestore Data Structure Step 4: Create the Blog Post Form Component Step 5: Create the Blog Posts List Component Step 6: Update the Home Component Step 7: Add Styling for New Components Step 8: Testing the Complete Flow Understanding How Firestore Works Security Rules Common Issues and Troubleshooting Next Steps Conclusion What We'll Build Building upon our authentication system, we'll add: A form for users to create blog posts with title, content, and category Real-time storage of blog posts in Firebase Firestore A list view to display all user's blog posts Real-time updates when new posts are added User-specific data isolation (users only see their own posts) Understanding Firebase Firestore Before we dive into the code, let's understand what Firebase Firestore is and why we use it. What is Firestore? Firebase Firestore is a flexible, scalable NoSQL document database for mobile, web, and server development. Unlike traditional SQL databases with tables and rows, Firestore stores data in documents and collections. Key Concepts: Documents: Individual records that contain data as key-value pairs Collections: Groups of documents (similar to tables in SQL) Real-time Updates: Firestore can notify your app when data changes Offline Support: Works even when users are offline Security Rules: Server-side rules that control data access Why Use Firestore? Real-time synchronization: Changes appear instantly across all connected devices Scalability: Automatically scales with your application Security: Built-in authentication integration and security rules Offline support: Works seamlessly offline and syncs when back online Prerequisites Completed the previous Firebase Authentication tutorial Basic understanding of React hooks (useState, useEffect) Your Firebase project with Authentication already set up Step 1: Enable Firestore in Firebase Console First, we need to enable Firestore in our Firebase project: Go to your Firebase Console Select your existing project In the left sidebar, click on "Firestore Database" Click "Create database" Choose "Start in test mode" for now (we'll discuss security rules later) Select a location for your database (choose the one closest to your users) Click "Done" Important: Starting in test mode means anyone can read and write to your database. We'll secure this later with proper security rules. Step 2: Update Firebase Configuration We need to import Firestore functions in our existing Firebase configuration file: // src/firebase.js import { initializeApp } from 'firebase/app'; import { getAuth } from 'firebase/auth'; import { getFirestore } from 'firebase/firestore'; const firebaseConfig = { // Your existing Firebase config apiKey: "your-api-key", authDomain: "your-project-id.firebaseapp.com", projectId: "your-project-id", storageBucket: "your-project-id.appspot.com", messagingSenderId: "your-messaging-sender-id", appId: "your-app-id" }; // Initialize Firebase const app = initializeApp(firebaseConfig); export const auth = getAuth(app); export const db = getFirestore(app); export default app; Step 3: Understanding Firestore Data Structure Before we write code, let's plan our data structure. For our blog posts, we'll use this structure: Collection: "posts" Document ID: auto-generated Document Data: { title: "My First Blog Post", content: "This is the content of my blog post...", category: "Technology", authorId: "user-uid-from-auth", authorEmail: "user@example.com", createdAt: timestamp, updatedAt: timestamp } Why This Structure? authorId: Links posts to specific users for security and filtering timestamps: Track when posts were created and modified authorEmail: Easy way to display who wrote the post category: Allows for future filtering and organization features Step 4: Create the Blog Post Form Component Let's create a component that allows users to create new blog posts: // src/components/CreatePost.js import React, { useState } from 'react'; import { collection, addDoc, serverTimestamp } from 'firebase/firestore'; import { db, auth } from '../firebase'; function CreatePost({ onPostCreated }) { const

May 25, 2025 - 13:20
 0
Firebase Firestore in React: Storing and Retrieving User Data

Building upon Firebase Authentication to Create a Complete Data-Driven Application

In our previous tutorial, we successfully implemented Firebase Authentication in a React application. Now that users can sign up and log in, the next logical step is to allow them to store and manage their data. In this tutorial, we'll integrate Firebase Firestore (Firebase's NoSQL database) to create a simple blog post system where authenticated users can create, store, and retrieve their blog posts.

Table of Contents

  1. What We'll Build
  2. Understanding Firebase Firestore
  3. Prerequisites
  4. Step 1: Enable Firestore in Firebase Console
  5. Step 2: Update Firebase Configuration
  6. Step 3: Understanding Firestore Data Structure
  7. Step 4: Create the Blog Post Form Component
  8. Step 5: Create the Blog Posts List Component
  9. Step 6: Update the Home Component
  10. Step 7: Add Styling for New Components
  11. Step 8: Testing the Complete Flow
  12. Understanding How Firestore Works
  13. Security Rules
  14. Common Issues and Troubleshooting
  15. Next Steps
  16. Conclusion

What We'll Build

Building upon our authentication system, we'll add:

  • A form for users to create blog posts with title, content, and category
  • Real-time storage of blog posts in Firebase Firestore
  • A list view to display all user's blog posts
  • Real-time updates when new posts are added
  • User-specific data isolation (users only see their own posts)

Understanding Firebase Firestore

Before we dive into the code, let's understand what Firebase Firestore is and why we use it.

What is Firestore?

Firebase Firestore is a flexible, scalable NoSQL document database for mobile, web, and server development. Unlike traditional SQL databases with tables and rows, Firestore stores data in documents and collections.

Key Concepts:

  • Documents: Individual records that contain data as key-value pairs
  • Collections: Groups of documents (similar to tables in SQL)
  • Real-time Updates: Firestore can notify your app when data changes
  • Offline Support: Works even when users are offline
  • Security Rules: Server-side rules that control data access

Why Use Firestore?

  1. Real-time synchronization: Changes appear instantly across all connected devices
  2. Scalability: Automatically scales with your application
  3. Security: Built-in authentication integration and security rules
  4. Offline support: Works seamlessly offline and syncs when back online

Prerequisites

  • Completed the previous Firebase Authentication tutorial
  • Basic understanding of React hooks (useState, useEffect)
  • Your Firebase project with Authentication already set up

Step 1: Enable Firestore in Firebase Console

First, we need to enable Firestore in our Firebase project:

  1. Go to your Firebase Console
  2. Select your existing project
  3. In the left sidebar, click on "Firestore Database"
  4. Click "Create database"
  5. Choose "Start in test mode" for now (we'll discuss security rules later)
  6. Select a location for your database (choose the one closest to your users)
  7. Click "Done"

Important: Starting in test mode means anyone can read and write to your database. We'll secure this later with proper security rules.

Step 2: Update Firebase Configuration

We need to import Firestore functions in our existing Firebase configuration file:

// src/firebase.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  // Your existing Firebase config
  apiKey: "your-api-key",
  authDomain: "your-project-id.firebaseapp.com",
  projectId: "your-project-id",
  storageBucket: "your-project-id.appspot.com",
  messagingSenderId: "your-messaging-sender-id",
  appId: "your-app-id"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export default app;

Step 3: Understanding Firestore Data Structure

Before we write code, let's plan our data structure. For our blog posts, we'll use this structure:

Collection: "posts"
Document ID: auto-generated
Document Data: {
  title: "My First Blog Post",
  content: "This is the content of my blog post...",
  category: "Technology",
  authorId: "user-uid-from-auth",
  authorEmail: "user@example.com",
  createdAt: timestamp,
  updatedAt: timestamp
}

Why This Structure?

  • authorId: Links posts to specific users for security and filtering
  • timestamps: Track when posts were created and modified
  • authorEmail: Easy way to display who wrote the post
  • category: Allows for future filtering and organization features

Step 4: Create the Blog Post Form Component

Let's create a component that allows users to create new blog posts:

// src/components/CreatePost.js
import React, { useState } from 'react';
import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
import { db, auth } from '../firebase';

function CreatePost({ onPostCreated }) {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  const [category, setCategory] = useState('General');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [success, setSuccess] = useState('');

  const categories = ['General', 'Technology', 'Lifestyle', 'Travel', 'Food', 'Other'];

  async function handleSubmit(e) {
    e.preventDefault();

    if (!title.trim() || !content.trim()) {
      setError('Please fill in both title and content');
      return;
    }

    if (!auth.currentUser) {
      setError('You must be logged in to create a post');
      return;
    }

    try {
      setError('');
      setSuccess('');
      setLoading(true);

      // Add document to the "posts" collection
      const docRef = await addDoc(collection(db, 'posts'), {
        title: title.trim(),
        content: content.trim(),
        category: category,
        authorId: auth.currentUser.uid,
        authorEmail: auth.currentUser.email,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp()
      });

      console.log('Document written with ID: ', docRef.id);

      // Reset form
      setTitle('');
      setContent('');
      setCategory('General');
      setSuccess('Post created successfully!');

      // Notify parent component
      if (onPostCreated) {
        onPostCreated();
      }

    } catch (error) {
      console.error('Error adding document: ', error);
      setError('Failed to create post: ' + error.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="create-post-container">
      <h3>Create New Blog Post</h3>

      {error && <div className="error-message">{error}</div>}
      {success && <div className="success-message">{success}</div>}

      <form onSubmit={handleSubmit} className="create-post-form">
        <div className="form-group">
          <label htmlFor="title">Title</label>
          <input
            type="text"
            id="title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            placeholder="Enter your blog post title"
            maxLength={100}
            required
          />
          <small>{title.length}/100 characterssmall>
        </div>

        <div className="form-group">
          <label htmlFor="category">Category</label>
          <select
            id="category"
            value={category}
            onChange={(e) => setCategory(e.target.value)}
          >
            {categories.map(cat => (
              <option key={cat} value={cat}>{cat}</option>
            ))}
          </select>
        </div>

        <div className="form-group">
          <label htmlFor="content">Content</label>
          <textarea
            id="content"
            value={content}
            onChange={(e) => setContent(e.target.value)}
            placeholder="Write your blog post content here..."
            rows={8}
            maxLength={2000}
            required
          />
          <small>{content.length}/2000 characterssmall>
        </div>

        <button 
          type="submit" 
          disabled={loading}
          className="submit-button"
        >
          {loading ? 'Publishing...' : 'Publish Post'}
        </button>
      </form>
    </div>
  );
}

export default CreatePost;

Step 5: Create the Blog Posts List Component

Now let's create a component to display all the user's blog posts:

// src/components/PostsList.js
import React, { useState, useEffect } from 'react';
import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
import { db, auth } from '../firebase';

function PostsList({ refreshTrigger }) {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');

  useEffect(() => {
    if (!auth.currentUser) {
      setLoading(false);
      return;
    }

    // Create a query to get posts for the current user, ordered by creation date
    const q = query(
      collection(db, 'posts'),
      where('authorId', '==', auth.currentUser.uid),
      orderBy('createdAt', 'desc')
    );

    // Set up real-time listener
    const unsubscribe = onSnapshot(q, 
      (querySnapshot) => {
        const postsArray = [];
        querySnapshot.forEach((doc) => {
          postsArray.push({
            id: doc.id,
            ...doc.data()
          });
        });
        setPosts(postsArray);
        setLoading(false);
        setError('');
      },
      (error) => {
        console.error('Error fetching posts:', error);
        setError('Failed to load posts: ' + error.message);
        setLoading(false);
      }
    );

    // Cleanup subscription on unmount
    return () => unsubscribe();
  }, [refreshTrigger]);

  function formatDate(timestamp) {
    if (!timestamp) return 'Unknown date';

    // Handle Firestore timestamp
    const date = timestamp.toDate ? timestamp.toDate() : new Date(timestamp);
    return date.toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'long',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit'
    });
  }

  if (loading) {
    return <div className="loading">Loading your posts...</div>;
  }

  if (error) {
    return <div className="error-message">{error}</div>;
  }

  if (posts.length === 0) {
    return (
      <div className="no-posts">
        <h3>No Blog Posts Yet</h3>
        <p>You haven't created any blog posts yet. Create your first post above!
      
); } return (

Your Blog Posts ({posts.length})

{posts.map((post) => (

{post.title}

{post.category}

{post.content.length > 150 ? post.content.substring(0, 150) + '...' : post.content}

Created: {formatDate(post.createdAt)} {post.updatedAt && post.updatedAt !== post.createdAt && ( Updated: {formatDate(post.updatedAt)} )}
))}
); } export default PostsList;

Step 6: Update the Home Component

Let's update our Home component to include both the create post form and posts list:

// src/components/Home.js
import React, { useState, useEffect } from 'react';
import { onAuthStateChanged, signOut } from 'firebase/auth';
import { auth } from '../firebase';
import { useNavigate } from 'react-router-dom';
import CreatePost from './CreatePost';
import PostsList from './PostsList';

function Home() {
  const [currentUser, setCurrentUser] = useState(null);
  const [refreshPosts, setRefreshPosts] = useState(0);
  const navigate = useNavigate();

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user) {
        setCurrentUser(user);
      } else {
        navigate('/login');
      }
    });

    return () => unsubscribe();
  }, [navigate]);

  async function handleLogout() {
    try {
      await signOut(auth);
      navigate('/login');
    } catch (error) {
      console.error('Failed to log out:', error);
    }
  }

  function handlePostCreated() {
    // Trigger refresh of posts list
    setRefreshPosts(prev => prev + 1);
  }

  if (!currentUser) return <div className="loading">Loading...</div>;

  return (
    <div className="home-container">
      <div className="home-header">
        <h2>Welcome to Your Blog Dashboard!</h2>
        <div className="user-info">
          <p>Logged in as: <strong>{currentUser.email}</strong>p>
          <button onClick={handleLogout} className="logout-button">
            Log Out
          </button>
        </div>
      </div>

      <div className="dashboard-content">
        <CreatePost onPostCreated={handlePostCreated} />
        <PostsList refreshTrigger={refreshPosts} />
      </div>
    </div>
  );
}

export default Home;

Step 7: Add Styling for New Components

Let's add CSS styles for our new components:

/* Add these styles to src/App.css */

/* Dashboard Layout */
.home-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  min-height: 100vh;
}

.home-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 30px;
  padding-bottom: 20px;
  border-bottom: 1px solid #eee;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 15px;
}

.dashboard-content {
  display: grid;
  gap: 30px;
}

/* Create Post Form */
.create-post-container {
  background: white;
  border-radius: 8px;
  padding: 25px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.create-post-container h3 {
  margin-bottom: 20px;
  color: #333;
}

.create-post-form {
  display: grid;
  gap: 15px;
}

.form-group {
  display: flex;
  flex-direction: column;
}

.form-group label {
  margin-bottom: 5px;
  font-weight: 500;
  color: #555;
}

.form-group input,
.form-group select,
.form-group textarea {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  font-family: inherit;
}

.form-group textarea {
  resize: vertical;
  min-height: 120px;
}

.form-group small {
  margin-top: 5px;
  color: #666;
  font-size: 12px;
}

/* Messages */
.success-message {
  background-color: #e8f5e8;
  color: #2e7d2e;
  padding: 10px;
  border-radius: 4px;
  margin-bottom: 15px;
  text-align: center;
}

.loading {
  text-align: center;
  padding: 20px;
  color: #666;
}

/* Posts List */
.posts-list-container {
  background: white;
  border-radius: 8px;
  padding: 25px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.posts-list-container h3 {
  margin-bottom: 20px;
  color: #333;
}

.no-posts {
  text-align: center;
  padding: 40px 20px;
  color: #666;
}

.no-posts h3 {
  margin-bottom: 10px;
  color: #999;
}

.posts-grid {
  display: grid;
  gap: 20px;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}

.post-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 20px;
  background: #fafafa;
  transition: transform 0.2s, box-shadow 0.2s;
}

.post-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.post-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 15px;
}

.post-title {
  color: #333;
  margin: 0;
  font-size: 18px;
  line-height: 1.3;
  flex: 1;
  margin-right: 10px;
}

.post-category {
  background: #4285f4;
  color: white;
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
  white-space: nowrap;
}

.post-content {
  margin-bottom: 15px;
}

.post-content p {
  color: #666;
  line-height: 1.5;
  margin: 0;
}

.post-footer {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-top: 15px;
  border-top: 1px solid #e0e0e0;
}

.post-date,
.post-updated {
  color: #999;
  font-size: 12px;
}

/* Responsive Design */
@media (max-width: 768px) {
  .home-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 15px;
  }

  .user-info {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }

  .posts-grid {
    grid-template-columns: 1fr;
  }

  .post-header {
    flex-direction: column;
    gap: 10px;
  }

  .post-category {
    align-self: flex-start;
  }
}

Step 8: Testing the Complete Flow

Now let's test our complete application:

npm start

Testing Steps:

  1. Authentication Test:

    • Sign up or log in to your application
    • Verify you're redirected to the dashboard
  2. Create Post Test:

    • Fill out the blog post form with a title, category, and content
    • Click "Publish Post"
    • Verify the success message appears
    • Check that the form resets after submission
  3. View Posts Test:

    • Verify your new post appears in the posts list below the form
    • Check that the post shows the correct title, content preview, category, and timestamp
  4. Real-time Updates Test:

    • Open your app in two browser tabs
    • Create a post in one tab
    • Verify it appears immediately in the other tab
  5. User Isolation Test:

    • Create a second user account
    • Verify that each user only sees their own posts

Understanding How Firestore Works

Data Flow Explanation

  1. Writing Data (Creating Posts):

    • When a user submits the form, we use addDoc() to add a new document to the "posts" collection
    • Firestore automatically generates a unique ID for each document
    • We include serverTimestamp() to ensure consistent timestamps across all clients
  2. Reading Data (Displaying Posts):

    • We use a query with where() to filter posts by the current user's ID
    • orderBy() sorts posts by creation date (newest first)
    • onSnapshot() creates a real-time listener that updates our component when data changes
  3. Real-time Updates:

    • The onSnapshot() listener automatically triggers when any matching document is added, modified, or deleted
    • This provides real-time synchronization across all connected clients

Key Firestore Functions Used

  • collection(db, 'posts'): References the "posts" collection
  • addDoc(): Adds a new document with auto-generated ID
  • query(): Creates a query to filter and sort data
  • where(): Filters documents based on field values
  • orderBy(): Sorts documents by specified fields
  • onSnapshot(): Sets up real-time listener for data changes
  • serverTimestamp(): Generates server-side timestamp

Security Rules

Currently, our database is in test mode, which means anyone can read and write data. Let's set up proper security rules:

  1. Go to Firebase Console → Firestore Database → Rules
  2. Replace the default rules with:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Users can only access their own posts
    match /posts/{postId} {
      allow read, write: if request.auth != null && request.auth.uid == resource.data.authorId;
      allow create: if request.auth != null && request.auth.uid == request.resource.data.authorId;
    }
  }
}

These rules ensure that:

  • Only authenticated users can access posts
  • Users can only read/write their own posts
  • The authorId field must match the authenticated user's ID

Common Issues and Troubleshooting

Firestore Security Rules Errors

Error: "Missing or insufficient permissions"
Solution: Check that your security rules allow the current user to access the data

Real-time Updates Not Working

Error: Posts don't appear immediately after creation
Solution: Ensure you're using onSnapshot() for real-time listening, not just a one-time read

Timestamp Issues

Error: Dates showing as "Unknown date"
Solution: Make sure you're handling both Firestore timestamps and JavaScript dates in your formatDate function

Authentication State Issues

Error: Posts not loading for authenticated users
Solution: Ensure auth.currentUser is available before creating Firestore queries

Next Steps

Now that you have a working blog system, consider adding:

  1. Edit and Delete Posts: Allow users to modify or remove their posts
  2. Search and Filter: Add search functionality and category filtering
  3. Pagination: Handle large numbers of posts efficiently
  4. Rich Text Editor: Upgrade from plain text to formatted content
  5. Image Uploads: Add Firebase Storage for image attachments
  6. Comments System: Allow users to comment on posts
  7. Public/Private Posts: Add visibility settings for posts

Conclusion

Congratulations! You've successfully integrated Firebase Firestore with your React authentication system. You now have a complete application where users can:

  • Sign up and log in securely
  • Create blog posts with real-time storage
  • View their posts with automatic updates
  • Enjoy user-specific data isolation

This foundation provides everything you need to build more complex data-driven applications. The combination of Firebase Authentication and Firestore gives you a powerful, scalable backend without managing servers.

The real-time capabilities of Firestore make your application feel responsive and modern, while the security rules ensure that user data remains private and secure.

Happy coding, and welcome to the world of real-time web applications!