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

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 [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!
Your Blog Posts ({posts.length})
{post.title}
{post.category}{post.content.length > 150 ? post.content.substring(0, 150) + '...' : post.content}
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:
-
Authentication Test:
- Sign up or log in to your application
- Verify you're redirected to the dashboard
-
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
-
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
-
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
-
User Isolation Test:
- Create a second user account
- Verify that each user only sees their own posts
Understanding How Firestore Works
Data Flow Explanation
-
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
- When a user submits the form, we use
-
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
- We use a query with
-
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
- The
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:
- Go to Firebase Console → Firestore Database → Rules
- 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:
- Edit and Delete Posts: Allow users to modify or remove their posts
- Search and Filter: Add search functionality and category filtering
- Pagination: Handle large numbers of posts efficiently
- Rich Text Editor: Upgrade from plain text to formatted content
- Image Uploads: Add Firebase Storage for image attachments
- Comments System: Allow users to comment on posts
- 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!