To detect changes in Firebase Realtime Database, use the onValue listener for full snapshots or onChildAdded, onChildChanged, and onChildRemoved for granular change detection. Attach listeners with the ref() and on-prefixed functions from the modular SDK, and always call off() or the returned unsubscribe function to prevent memory leaks when the component unmounts.
Listening for Real-Time Changes in Firebase Realtime Database
Firebase Realtime Database synchronizes data across connected clients instantly. This tutorial covers every listener type in the modular SDK v9+ syntax: onValue for full snapshots, and the child event listeners for tracking additions, updates, and deletions individually. You will learn when to use each listener type and how to clean up properly in React applications.
Prerequisites
- A Firebase project with Realtime Database enabled
- The firebase npm package installed (v9 or later)
- Firebase initialized in your app with a valid config object
- Basic knowledge of JavaScript/TypeScript and React hooks
Step-by-step guide
Initialize the Realtime Database reference
Initialize the Realtime Database reference
Import the Realtime Database functions from the firebase/database module. Use getDatabase() to get a database instance and ref() to create a reference to the path you want to listen to. The reference is a pointer to a location in your database tree and does not fetch data on its own.
1import { initializeApp } from 'firebase/app'2import { getDatabase, ref } from 'firebase/database'34const firebaseConfig = {5 apiKey: 'your-api-key',6 authDomain: 'your-project.firebaseapp.com',7 databaseURL: 'https://your-project-default-rtdb.firebaseio.com',8 projectId: 'your-project'9}1011const app = initializeApp(firebaseConfig)12const db = getDatabase(app)1314// Reference to the messages node15const messagesRef = ref(db, 'messages')Expected result: A database reference is created pointing to the 'messages' path.
Listen for full value changes with onValue
Listen for full value changes with onValue
The onValue listener fires immediately with the current data at the reference path, then fires again whenever any data under that path changes. The callback receives a DataSnapshot object. Use snapshot.val() to get the JavaScript object representation of the data. If no data exists at the path, snapshot.val() returns null. This listener is best for monitoring an entire node and all its children.
1import { onValue } from 'firebase/database'23const unsubscribe = onValue(messagesRef, (snapshot) => {4 if (snapshot.exists()) {5 const data = snapshot.val()6 console.log('Current messages:', data)7 } else {8 console.log('No messages found')9 }10}, (error) => {11 console.error('Listener error:', error.message)12})Expected result: The callback fires immediately with existing data, then fires again on every change to the messages node.
Track individual child events for granular updates
Track individual child events for granular updates
Instead of receiving the entire data set on every change, use child event listeners to detect specific types of changes. onChildAdded fires for each existing child initially and then for every new child added. onChildChanged fires when an existing child's value changes. onChildRemoved fires when a child is deleted. These are more efficient for lists because they tell you exactly what changed.
1import { onChildAdded, onChildChanged, onChildRemoved } from 'firebase/database'23const unsubAdded = onChildAdded(messagesRef, (snapshot) => {4 console.log('New message:', snapshot.key, snapshot.val())5})67const unsubChanged = onChildChanged(messagesRef, (snapshot) => {8 console.log('Updated message:', snapshot.key, snapshot.val())9})1011const unsubRemoved = onChildRemoved(messagesRef, (snapshot) => {12 console.log('Deleted message:', snapshot.key, snapshot.val())13})Expected result: Each listener fires for its respective event type: additions, modifications, and deletions.
Unsubscribe from listeners in React useEffect
Unsubscribe from listeners in React useEffect
In React, attach listeners inside useEffect and return the unsubscribe function in the cleanup. This ensures listeners are removed when the component unmounts, preventing memory leaks and stale state updates. If you use multiple child listeners, unsubscribe from all of them in the cleanup function.
1import { useEffect, useState } from 'react'2import { getDatabase, ref, onValue } from 'firebase/database'34interface Message {5 id: string6 text: string7 timestamp: number8}910function useMessages() {11 const [messages, setMessages] = useState<Message[]>([])1213 useEffect(() => {14 const db = getDatabase()15 const messagesRef = ref(db, 'messages')1617 const unsubscribe = onValue(messagesRef, (snapshot) => {18 const data = snapshot.val()19 if (!data) {20 setMessages([])21 return22 }23 const list = Object.entries(data).map(([id, value]) => ({24 id,25 ...(value as Omit<Message, 'id'>),26 }))27 setMessages(list)28 })2930 return () => unsubscribe()31 }, [])3233 return messages34}Expected result: The hook provides a live-updating messages array and cleans up the listener on unmount.
Filter and order listened data with queries
Filter and order listened data with queries
Combine listeners with query constraints to listen to a subset of data. Use orderByChild, limitToLast, or equalTo to create a query, then pass it to onValue or child event listeners. This reduces bandwidth by only syncing the data you need instead of the entire node.
1import { query, orderByChild, limitToLast, onValue } from 'firebase/database'23const recentMessagesQuery = query(4 messagesRef,5 orderByChild('timestamp'),6 limitToLast(25)7)89const unsubscribe = onValue(recentMessagesQuery, (snapshot) => {10 const messages: Message[] = []11 snapshot.forEach((child) => {12 messages.push({ id: child.key!, ...child.val() })13 })14 console.log('Last 25 messages:', messages)15})Expected result: Only the 25 most recent messages are synced and updated in real time.
Complete working example
1import { useEffect, useState } from 'react'2import { getDatabase, ref, query, orderByChild, limitToLast, onValue, onChildAdded, onChildChanged, onChildRemoved } from 'firebase/database'34export interface Message {5 id: string6 text: string7 author: string8 timestamp: number9}1011export function useRealtimeMessages(limit: number = 50) {12 const [messages, setMessages] = useState<Message[]>([])13 const [loading, setLoading] = useState(true)14 const [error, setError] = useState<string | null>(null)1516 useEffect(() => {17 const db = getDatabase()18 const messagesRef = ref(db, 'messages')19 const messagesQuery = query(20 messagesRef,21 orderByChild('timestamp'),22 limitToLast(limit)23 )2425 const unsubscribe = onValue(26 messagesQuery,27 (snapshot) => {28 if (!snapshot.exists()) {29 setMessages([])30 setLoading(false)31 return32 }3334 const list: Message[] = []35 snapshot.forEach((child) => {36 list.push({37 id: child.key!,38 ...(child.val() as Omit<Message, 'id'>),39 })40 })4142 setMessages(list)43 setLoading(false)44 setError(null)45 },46 (err) => {47 setError(err.message)48 setLoading(false)49 }50 )5152 return () => unsubscribe()53 }, [limit])5455 return { messages, loading, error }56}Common mistakes when detecting Changes in Firebase Realtime Database
Why it's a problem: Forgetting to unsubscribe from listeners, causing memory leaks and state updates on unmounted components
How to avoid: Always store the return value of onValue or child listeners and call it in your useEffect cleanup function.
Why it's a problem: Using onValue on a large node without query constraints, downloading the entire dataset on every change
How to avoid: Apply limitToLast or limitToFirst to reduce the data synced. For lists, use child event listeners which only send individual changes.
Why it's a problem: Not handling the null case when snapshot.val() returns null for empty paths
How to avoid: Always check snapshot.exists() before calling snapshot.val(). An empty path returns null, and iterating over null causes runtime errors.
Best practices
- Use onValue for single-document monitoring and child event listeners for lists of items
- Always unsubscribe from listeners when the component unmounts or the listened path changes
- Add .indexOn rules for any field you use with orderByChild to avoid performance warnings
- Use limitToLast or limitToFirst to cap the number of items synced in real time
- Handle the error callback in listeners to catch permission-denied and network errors
- Use snapshot.forEach() instead of Object.entries(snapshot.val()) to preserve server-side ordering
- Debounce rapid UI updates when onValue fires frequently to avoid unnecessary re-renders
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to listen for real-time changes in a Firebase Realtime Database 'messages' node using the modular SDK v9. Show me how to use onValue for full snapshots and onChildAdded/Changed/Removed for individual updates, with proper cleanup in a React useEffect hook.
Set up real-time listeners on the 'messages' node in Firebase Realtime Database using the modular SDK. Create a React custom hook that listens for value changes with onValue, supports query ordering by timestamp, and properly unsubscribes on cleanup.
Frequently asked questions
What is the difference between onValue and onChildAdded?
onValue fires with the entire snapshot of data at a path whenever anything changes. onChildAdded fires once for each existing child initially and then once for each new child added. Use onValue for single values and onChildAdded for lists.
Does onValue fire when the app first connects?
Yes, onValue fires immediately with the current data when first attached. This means you get both the initial load and all subsequent updates through the same callback.
How do I detect only new items without getting existing data?
onChildAdded fires for all existing children first. To detect only new additions, store a timestamp and filter out items with timestamps before your listener attached. Alternatively, use limitToLast(1) and skip the first callback.
Can I listen to deeply nested paths?
Yes, create a ref to any path like ref(db, 'users/userId/profile') and attach a listener. The listener only receives data at that path and below, not the entire database.
What happens to listeners when the device goes offline?
Listeners continue to fire against locally cached data. When the device reconnects, Firebase syncs any changes that occurred while offline and fires the listeners with updated data.
Can RapidDev help implement real-time data sync for my application?
Yes, RapidDev's engineering team can architect real-time data flows using Firebase Realtime Database or Firestore, including proper listener management and offline handling.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation