What is Firebase Realtime Database?
The Firebase Realtime Database is a cloud-hosted NoSQL database that stores data as JSON and synchronizes it in real-time to every connected client. When you build cross-platform apps, all clients share one database instance and automatically receive updates with the newest data.
While Cloud Firestore is the newer database option, Realtime Database is still excellent for simpler data structures, lower latency requirements, and scenarios where you need very fast writes.
⚡ Realtime Database vs Cloud Firestore
Realtime Database
- • Data stored as one large JSON tree
- • Lower latency for simple operations
- • Charged per data transferred
- • Single region only
- • Simpler querying
Cloud Firestore
- • Data stored in documents and collections
- • More complex queries supported
- • Charged per operation
- • Multi-region available
- • Better scaling for complex apps
Setting Up Realtime Database
Initialize the Realtime Database in your application:
import { initializeApp } from 'firebase/app';
import { getDatabase } from 'firebase/database';
const firebaseConfig = {
// Your config here
databaseURL: 'https://your-project.firebaseio.com'
};
const app = initializeApp(firebaseConfig);
const database = getDatabase(app);
export { database };
Understanding the JSON Structure
Realtime Database stores data as a single JSON tree:
// Example database structure
{
"users": {
"user1": {
"name": "Alice",
"email": "alice@example.com",
"profile": {
"avatar": "https://...",
"bio": "Developer"
}
},
"user2": {
"name": "Bob",
"email": "bob@example.com"
}
},
"posts": {
"post1": {
"title": "Hello World",
"author": "user1",
"timestamp": 1634567890
}
},
"user-posts": {
"user1": {
"post1": true,
"post2": true
}
}
}
Writing Data
There are several ways to write data to the Realtime Database:
Set Data (Overwrites)
import { ref, set } from 'firebase/database';
// Write to a specific path (overwrites existing data)
async function writeUserData(userId, name, email) {
await set(ref(database, 'users/' + userId), {
name: name,
email: email,
createdAt: Date.now()
});
console.log('User data saved');
}
writeUserData('user123', 'John Doe', 'john@example.com');
Push Data (Auto-generated Key)
import { ref, push, set } from 'firebase/database';
// Create a new child with auto-generated key
async function addPost(title, content, authorId) {
const postsRef = ref(database, 'posts');
const newPostRef = push(postsRef);
await set(newPostRef, {
title: title,
content: content,
author: authorId,
timestamp: Date.now()
});
console.log('New post ID:', newPostRef.key);
return newPostRef.key;
}
Update Data (Partial Update)
import { ref, update } from 'firebase/database';
// Update specific fields without overwriting others
async function updateUserProfile(userId, updates) {
const userRef = ref(database, 'users/' + userId);
await update(userRef, {
name: updates.name,
'profile/bio': updates.bio, // Update nested path
updatedAt: Date.now()
});
}
// Multi-path updates (atomic)
async function publishPost(postId, userId) {
const updates = {};
updates['/posts/' + postId + '/published'] = true;
updates['/user-posts/' + userId + '/' + postId] = true;
await update(ref(database), updates);
}
Reading Data
Read data once or listen for real-time updates:
Read Once
import { ref, get, child } from 'firebase/database';
// Read data once
async function getUser(userId) {
const dbRef = ref(database);
const snapshot = await get(child(dbRef, 'users/' + userId));
if (snapshot.exists()) {
console.log('User data:', snapshot.val());
return snapshot.val();
} else {
console.log('No user found');
return null;
}
}
Listen for Real-time Updates
import { ref, onValue, off } from 'firebase/database';
// Subscribe to changes
function subscribeToUser(userId, callback) {
const userRef = ref(database, 'users/' + userId);
const unsubscribe = onValue(userRef, (snapshot) => {
if (snapshot.exists()) {
callback(snapshot.val());
} else {
callback(null);
}
});
// Return function to stop listening
return () => off(userRef);
}
// Usage
const unsubscribe = subscribeToUser('user123', (userData) => {
console.log('User updated:', userData);
});
// Later, stop listening
unsubscribe();
Listening for Child Events
Listen for specific changes to child nodes:
import { ref, onChildAdded, onChildChanged, onChildRemoved } from 'firebase/database';
const postsRef = ref(database, 'posts');
// New child added
onChildAdded(postsRef, (snapshot) => {
console.log('New post:', snapshot.key, snapshot.val());
});
// Child changed
onChildChanged(postsRef, (snapshot) => {
console.log('Post updated:', snapshot.key, snapshot.val());
});
// Child removed
onChildRemoved(postsRef, (snapshot) => {
console.log('Post deleted:', snapshot.key);
});
Querying Data
Filter and sort data with queries:
import {
ref,
query,
orderByChild,
orderByKey,
orderByValue,
limitToFirst,
limitToLast,
startAt,
endAt,
equalTo,
get
} from 'firebase/database';
// Order by a child property
async function getRecentPosts(limit = 10) {
const postsRef = ref(database, 'posts');
const q = query(
postsRef,
orderByChild('timestamp'),
limitToLast(limit)
);
const snapshot = await get(q);
const posts = [];
snapshot.forEach((child) => {
posts.push({ id: child.key, ...child.val() });
});
return posts.reverse(); // Most recent first
}
// Filter by value
async function getPostsByAuthor(authorId) {
const postsRef = ref(database, 'posts');
const q = query(
postsRef,
orderByChild('author'),
equalTo(authorId)
);
const snapshot = await get(q);
const posts = [];
snapshot.forEach((child) => {
posts.push({ id: child.key, ...child.val() });
});
return posts;
}
// Range queries
async function getUsersInRange(startName, endName) {
const usersRef = ref(database, 'users');
const q = query(
usersRef,
orderByChild('name'),
startAt(startName),
endAt(endName)
);
const snapshot = await get(q);
return snapshot.val();
}
Deleting Data
Remove data from the database:
import { ref, remove, set } from 'firebase/database';
// Remove a node and all its children
async function deleteUser(userId) {
await remove(ref(database, 'users/' + userId));
console.log('User deleted');
}
// Alternative: set to null
async function deletePost(postId) {
await set(ref(database, 'posts/' + postId), null);
}
// Delete multiple paths atomically
async function deleteUserCompletely(userId) {
const updates = {};
updates['/users/' + userId] = null;
updates['/user-posts/' + userId] = null;
await update(ref(database), updates);
}
Transactions
Handle concurrent updates safely with transactions:
import { ref, runTransaction } from 'firebase/database';
// Increment a counter safely
async function incrementLikes(postId) {
const likesRef = ref(database, 'posts/' + postId + '/likes');
await runTransaction(likesRef, (currentLikes) => {
// If null, initialize to 0
return (currentLikes || 0) + 1;
});
}
// More complex transaction
async function toggleLike(postId, userId) {
const postRef = ref(database, 'posts/' + postId);
const result = await runTransaction(postRef, (post) => {
if (post) {
if (!post.likedBy) {
post.likedBy = {};
}
if (post.likedBy[userId]) {
// Unlike
post.likes = (post.likes || 1) - 1;
delete post.likedBy[userId];
} else {
// Like
post.likes = (post.likes || 0) + 1;
post.likedBy[userId] = true;
}
}
return post;
});
return result.committed;
}
Offline Capabilities
Enable offline persistence:
import { getDatabase, enablePersistence } from 'firebase/database';
// Enable offline persistence (web)
// Note: This is enabled by default on mobile SDKs
// For web, use keepSynced to ensure data is available offline
import { ref, onDisconnect, keepSynced } from 'firebase/database';
// Keep a path synced for offline access
const postsRef = ref(database, 'posts');
keepSynced(postsRef, true);
// Handle disconnection
const presenceRef = ref(database, 'users/' + userId + '/online');
set(presenceRef, true);
// Set value when user disconnects
onDisconnect(presenceRef).set(false);
// Or remove on disconnect
const connectionRef = ref(database, 'connections/' + odId
onDisconnect(connectionRef).remove();
React Hook for Realtime Database
Create a custom hook for React:
import { useState, useEffect } from 'react';
import { ref, onValue, off } from 'firebase/database';
import { database } from '../lib/firebase';
export function useRealtimeData(path) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const dataRef = ref(database, path);
const handleData = (snapshot) => {
setData(snapshot.val());
setLoading(false);
};
const handleError = (err) => {
setError(err);
setLoading(false);
};
onValue(dataRef, handleData, handleError);
return () => off(dataRef);
}, [path]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useRealtimeData(`users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>User not found</div>;
return <div>Welcome, {user.name}!</div>;
}
💡 Key Takeaways
- • Realtime Database stores data as a single JSON tree
- • Use set for overwriting, update for partial updates, push for auto-IDs
- • onValue provides real-time synchronization
- • Transactions ensure safe concurrent updates
- • Structure data flat to avoid deeply nested reads
- • Use indexes in rules for query performance
📚 Learn More
-
Realtime Database Documentation →
Official guides for Firebase Realtime Database
-
Structure Your Database →
Best practices for organizing JSON data
-
Offline Capabilities →
Enable offline data persistence