Read data from Firebase Realtime Database using the modular SDK v9+ with ref() to target a path and get() for one-time reads or onValue() for real-time listeners. Use orderByChild(), equalTo(), limitToFirst(), and limitToLast() to filter and sort results. One-time reads with get() are best for data that rarely changes, while onValue() provides instant updates whenever the data at the referenced path changes.
Reading Data from Firebase Realtime Database
Firebase Realtime Database stores data as a single JSON tree. You access specific parts of that tree using references, which are paths like 'users/uid123/profile'. This tutorial covers one-time data fetches with get(), real-time listeners with onValue(), ordering and filtering with query constraints, and properly cleaning up listeners. All examples use the modular SDK v9+ import style.
Prerequisites
- A Firebase project with Realtime Database enabled
- Firebase JS SDK v9+ installed (npm install firebase)
- Data already written to your Realtime Database
- Basic JavaScript or TypeScript knowledge
Step-by-step guide
Initialize Realtime Database and read data once
Initialize Realtime Database and read data once
Import the required functions from the firebase/database module and initialize your database instance. Use ref() to create a reference to a specific path in your database, then call get() to fetch the data at that path once. The returned snapshot has a val() method that returns the JavaScript object stored at that location. Always check snapshot.exists() before calling val() to handle missing data.
1import { initializeApp } from 'firebase/app'2import { getDatabase, ref, get, child } from 'firebase/database'34const app = initializeApp({5 apiKey: 'YOUR_API_KEY',6 authDomain: 'YOUR_PROJECT.firebaseapp.com',7 projectId: 'YOUR_PROJECT_ID',8 databaseURL: 'https://YOUR_PROJECT.firebaseio.com'9})1011const db = getDatabase(app)1213async function readUserProfile(userId: string) {14 const snapshot = await get(ref(db, `users/${userId}/profile`))1516 if (snapshot.exists()) {17 console.log('Profile:', snapshot.val())18 return snapshot.val()19 } else {20 console.log('No profile found for this user.')21 return null22 }23}Expected result: The function returns the user profile object if it exists, or null with a console message if the path has no data.
Listen for real-time updates with onValue()
Listen for real-time updates with onValue()
Use onValue() to subscribe to a database path and receive updates in real time. The callback fires immediately with the current data, then again every time the data at that path (or any child path) changes. Store the unsubscribe function returned by onValue() and call it when you no longer need updates. In the callback, always check snapshot.exists() because the path could be deleted.
1import { getDatabase, ref, onValue } from 'firebase/database'23const db = getDatabase()45function listenToMessages(chatId: string) {6 const messagesRef = ref(db, `chats/${chatId}/messages`)78 const unsubscribe = onValue(messagesRef, (snapshot) => {9 if (snapshot.exists()) {10 const messages = snapshot.val()11 // messages is an object with push keys as properties12 const messageList = Object.entries(messages).map(13 ([key, value]: [string, any]) => ({14 id: key,15 ...value16 })17 )18 console.log('Messages:', messageList)19 } else {20 console.log('No messages yet.')21 }22 })2324 // Return unsubscribe for cleanup25 return unsubscribe26}Expected result: The callback fires immediately with all existing messages and then re-fires whenever a message is added, changed, or deleted.
Listen for specific child events
Listen for specific child events
Instead of receiving the entire dataset on every change, use onChildAdded(), onChildChanged(), and onChildRemoved() to listen for specific types of changes. onChildAdded fires once for each existing child and then for each new child. onChildChanged fires when any child's data changes. onChildRemoved fires when a child is deleted. This is more efficient than onValue for large lists.
1import { getDatabase, ref, onChildAdded, onChildChanged, onChildRemoved } from 'firebase/database'23const db = getDatabase()4const messagesRef = ref(db, 'chats/room1/messages')56const unsubAdded = onChildAdded(messagesRef, (snapshot) => {7 console.log('New message:', snapshot.key, snapshot.val())8})910const unsubChanged = onChildChanged(messagesRef, (snapshot) => {11 console.log('Updated message:', snapshot.key, snapshot.val())12})1314const unsubRemoved = onChildRemoved(messagesRef, (snapshot) => {15 console.log('Deleted message:', snapshot.key, snapshot.val())16})1718// Cleanup all listeners19// unsubAdded(); unsubChanged(); unsubRemoved();Expected result: Each listener fires only for its specific event type, enabling efficient incremental UI updates.
Filter and sort data with query constraints
Filter and sort data with query constraints
Wrap a ref in query() with ordering and filtering constraints. Use orderByChild() to sort by a child key, then equalTo() to filter to a specific value, or limitToFirst()/limitToLast() to cap results. Realtime Database supports one orderBy per query. The snapshot.forEach() method iterates children in the sorted order.
1import { getDatabase, ref, query, orderByChild, equalTo, limitToFirst, get } from 'firebase/database'23const db = getDatabase()45// Get all users in the 'admin' role6async function getAdminUsers() {7 const usersQuery = query(8 ref(db, 'users'),9 orderByChild('role'),10 equalTo('admin')11 )1213 const snapshot = await get(usersQuery)14 const admins: any[] = []15 snapshot.forEach((child) => {16 admins.push({ id: child.key, ...child.val() })17 })18 return admins19}2021// Get the 10 most recent posts22async function getRecentPosts() {23 const postsQuery = query(24 ref(db, 'posts'),25 orderByChild('timestamp'),26 limitToLast(10)27 )2829 const snapshot = await get(postsQuery)30 const posts: any[] = []31 snapshot.forEach((child) => {32 posts.push({ id: child.key, ...child.val() })33 })34 return posts.reverse() // limitToLast returns ascending, reverse for newest first35}Expected result: getAdminUsers returns only users with role 'admin'. getRecentPosts returns the 10 newest posts sorted by timestamp.
Handle errors and offline behavior
Handle errors and offline behavior
Pass an error callback as the third argument to onValue() to handle permission errors or connectivity issues. Realtime Database has built-in offline support — it caches data locally and serves reads from cache when offline. Use the .info/connected path to monitor connection state and show online/offline status in your UI.
1import { getDatabase, ref, onValue } from 'firebase/database'23const db = getDatabase()45// Listen with error handling6onValue(7 ref(db, 'protectedData'),8 (snapshot) => {9 console.log('Data:', snapshot.val())10 },11 (error) => {12 console.error('Permission denied or network error:', error.message)13 }14)1516// Monitor connection state17onValue(ref(db, '.info/connected'), (snapshot) => {18 const isConnected = snapshot.val() === true19 console.log(isConnected ? 'Connected' : 'Disconnected')20})Expected result: The error callback logs permission or network errors. The connection monitor logs the current connectivity state.
Complete working example
1import { initializeApp } from 'firebase/app'2import {3 getDatabase,4 ref,5 get,6 query,7 orderByChild,8 equalTo,9 limitToFirst,10 limitToLast,11 onValue,12 onChildAdded,13 onChildRemoved,14 Unsubscribe15} from 'firebase/database'1617const app = initializeApp({18 apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,19 authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,20 projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,21 databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL!22})2324const db = getDatabase(app)2526// One-time read27export async function readOnce<T>(path: string): Promise<T | null> {28 const snapshot = await get(ref(db, path))29 return snapshot.exists() ? (snapshot.val() as T) : null30}3132// Filtered query33export async function queryByChild<T>(34 path: string,35 childKey: string,36 value: string | number | boolean,37 maxResults?: number38): Promise<T[]> {39 let q = query(ref(db, path), orderByChild(childKey), equalTo(value))40 if (maxResults) {41 q = query(ref(db, path), orderByChild(childKey), equalTo(value), limitToFirst(maxResults))42 }4344 const snapshot = await get(q)45 const results: T[] = []46 snapshot.forEach((child) => {47 results.push({ id: child.key, ...child.val() } as T)48 })49 return results50}5152// Real-time listener53export function subscribe(54 path: string,55 callback: (data: any) => void56): Unsubscribe {57 return onValue(58 ref(db, path),59 (snapshot) => callback(snapshot.exists() ? snapshot.val() : null),60 (error) => console.error(`Listener error at ${path}:`, error.message)61 )62}6364// Connection state monitor65export function onConnectionChange(66 callback: (connected: boolean) => void67): Unsubscribe {68 return onValue(ref(db, '.info/connected'), (snapshot) => {69 callback(snapshot.val() === true)70 })71}Common mistakes when reading Data from Firebase Realtime Database
Why it's a problem: Forgetting to include databaseURL in the Firebase config, causing 'Cannot read properties of undefined' errors
How to avoid: Add databaseURL: 'https://YOUR_PROJECT.firebaseio.com' to your Firebase config object. Find the exact URL in Firebase Console under Project Settings.
Why it's a problem: Not calling the unsubscribe function from onValue(), causing memory leaks and duplicate listeners
How to avoid: Store the return value of onValue() and call it when the listener is no longer needed. In React, return it from useEffect's cleanup function.
Why it's a problem: Using orderByChild() without an .indexOn rule, causing the entire dataset to be downloaded and sorted client-side
How to avoid: Add an .indexOn rule in your database rules: { "users": { ".indexOn": ["role", "email"] } }. This lets the server filter and sort before sending data.
Best practices
- Use get() for data you read once and onValue() only when you need real-time updates to minimize bandwidth usage
- Always check snapshot.exists() before calling snapshot.val() to handle deleted or missing data
- Add .indexOn rules for every field you query with orderByChild() to avoid full dataset downloads
- Use onChildAdded/Changed/Removed instead of onValue for large lists to receive incremental updates
- Keep your database structure shallow — deeply nested data forces large downloads when you only need part of the tree
- Monitor .info/connected to show users their online/offline status and queue writes when disconnected
- Use limitToFirst() or limitToLast() to cap the number of results and reduce bandwidth costs
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Firebase Realtime Database with a 'users' node containing user profiles and a 'messages' node with chat messages. Show me how to read data using the Firebase modular SDK v9+, including one-time reads with get(), real-time listeners with onValue(), filtering with orderByChild and equalTo, and proper cleanup of listeners.
Create a TypeScript utility module for reading Firebase Realtime Database data using modular SDK v9+. Include type-safe one-time reads, real-time subscriptions with error handling, filtered queries using orderByChild/equalTo/limitToLast, and connection state monitoring.
Frequently asked questions
What is the difference between Realtime Database and Firestore?
Realtime Database stores data as a single JSON tree with bandwidth-based pricing and excels at low-latency simple updates like presence and typing indicators. Firestore uses a document-collection model with operation-based pricing and supports more complex queries, offline web persistence, and better scaling beyond 200,000 concurrent connections.
Does Realtime Database work offline?
Yes. Realtime Database automatically caches data locally and serves reads from the cache when the device is offline. When connectivity is restored, it syncs pending writes and updates the local cache with server changes.
How many simultaneous listeners can I have?
There is no hard limit on the number of listeners, but each listener maintains an open connection. The Spark free plan allows 100 simultaneous connections per database. Blaze allows up to 200,000. Each browser tab counts as one connection regardless of how many listeners are active.
Can I query by multiple fields in Realtime Database?
Realtime Database supports only one orderByChild per query. For multi-field queries, create a composite key field (like 'category_price') that combines the values, or use Firestore which natively supports compound queries.
How do I paginate results in Realtime Database?
Use limitToFirst() or limitToLast() with startAt() or endAt() to implement cursor-based pagination. After fetching a page, use the last item's sort value as the starting point for the next query.
Can RapidDev help me implement real-time data reading in my Firebase app?
Yes. RapidDev can set up efficient Realtime Database read patterns including real-time listeners, optimized queries with proper indexing rules, and offline-capable data synchronization tailored to your application.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation