`snapshot.forEach()` fails on snapshots from `child_added` events when using `orderByChild()` queries
Operating System
macOS Ventura 13.4, Darwin 24.4.0
Environment (if applicable)
Node.js v20.18.0
Firebase SDK Version
12.0.0
Firebase SDK Product(s)
Database
Project Tooling
TypeScript, Jest testing framework
Detailed Problem Description
When using Firebase Realtime Database with orderByChild() queries, calling snapshot.forEach() on snapshots received from child_added events throws an error: "No index defined for <key>". This occurs even when the database is offline or when proper indexes are defined in security rules.
What I was trying to achieve:
- Listen for
child_addedevents on a query ordered by a child field - Iterate through the snapshot's children using
snapshot.forEach()
What actually happened:
- The
child_addedevent fires correctly and the snapshot contains the expected data - Calling
snapshot.forEach()throws:Error: No index defined for createdAt - The same
forEach()call works perfectly when called on a fresh snapshot obtained viaget(snapshot.ref)
Error message:
BUG: No index defined for createdAt
Steps and code to reproduce issue
import { initializeApp } from "firebase/app";
import {
getDatabase,
ref,
set,
get,
query,
orderByChild,
onChildAdded,
goOffline,
} from "firebase/database";
const app = initializeApp({
databaseURL: "https://test-bug-repro.firebaseio.com",
}, "bug-repro-modern");
const db = getDatabase(app);
goOffline(db);
const testData = {
items: {
item1: {
createdAt: 1741445778252,
},
},
};
async function reproduceOrderByChildForEachBug() {
set(ref(db), testData);
const itemsRef = ref(db, "items");
const q = query(itemsRef, orderByChild("createdAt"));
onChildAdded(q, async (snapshot) => {
console.log(`Received onChildAdded for: ${snapshot.key}`);
// This forEach call fails
try {
snapshot.forEach((child) => {
console.log(`Child: ${child.key}`);
return undefined;
});
} catch (error) {
console.error(`BUG: ${error.message}`); // "No index defined for createdAt"
}
// This workaround works
const freshSnapshot = await get(snapshot.ref);
freshSnapshot.forEach((child) => {
console.log(`Child (fresh): ${child.key}`);
return undefined;
});
});
}
reproduceOrderByChildForEachBug();
Expected vs Actual Behavior
Expected behavior: snapshot.forEach() should work on snapshots from child_added events just as it works on fresh snapshots.
Actual behavior: snapshot.forEach() throws an index error on snapshots from child_added events, requiring a workaround of fetching a fresh snapshot first.
Workaround
The current workaround is to fetch a fresh snapshot before calling forEach():
// Instead of calling forEach directly on the child_added snapshot:
// snapshot.forEach(callback); // ❌ Fails
// Use this workaround:
const freshSnapshot = await get(snapshot.ref);
freshSnapshot.forEach(callback); // ✅ Works
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
Hi @pokey, thanks for reaching out to us and apologies for the late response. I was able to replicate the reported behavior using your given code snippet. I did some investigation and here are some pointers why this is encountered:
onChildAddedevent is to notify you of one new child (or one initial child). TheDataSnapshotpassed to the callback represents only that single child node.forEachmethod is designed to iterate over multiple children contained within a parent snapshot.
When using onChildAdded, the snapshot you receive is always a snapshot of a single child, regardless of how many items are initially loaded or how the query is ordered. Therefore, we should not use forEach(). Instead, you access the single child's data directly.
Here is an example:
// src/OrderedListener.jsx
import React, { useEffect, useState } from 'react';
import { ref, query, orderByChild, onChildAdded, push, set } from "firebase/database";
import { db } from './firebase';
const DB_PATH = 'orderedItems'; // Path for the list
const itemsRef = ref(db, DB_PATH);
const pushSampleData = async () => {
const now = Date.now();
await push(itemsRef, {
order: 1,
name: 'Item C (Earliest)',
createdAt: now - 1000,
});
await push(itemsRef, {
order: 2,
name: 'Item B (Middle)',
createdAt: now + 500,
});
await push(itemsRef, {
order: 3,
name: 'Item A (Latest)',
createdAt: now + 2000,
});
console.log("Sample Data Pushed. Check the list order below.");
};
export default function OrderedListener() {
const [orderedList, setOrderedList] = useState([]);
useEffect(() => {
pushSampleData();
const orderedQuery = query(itemsRef, orderByChild('createdAt'));
const unsubscribe = onChildAdded(orderedQuery, (childSnapshot) => {
const newItem = {
id: childSnapshot.key,
...childSnapshot.val()
};
setOrderedList(prevList => [...prevList, newItem]);
});
return () => unsubscribe();
}, []);
return (
<div>
<h1>Firebase Ordered List (by `createdAt`)</h1>
<hr />
{orderedList.length === 0 ? (
<p>Loading items...</p>
) : (
<ul>
{orderedList.map(item => (
<li key={item.id}>
<strong>Order: {item.order}</strong> | Key: {item.id} | Name: {item.name} | CreatedAt: {item.createdAt}
</li>
))}
</ul>
)}
</div>
);
}
Could you give this code a try?
Hi @jbalidiong! Thank you for your response
I'm not sure that will work for my use case. I have code that recursively descends the snapshot structure, expecting a snapshot at each level, so that I can eg get a pointer back to the associated db ref. I believe your code is converting to a pojo so loses the snapshot info.
Is there a way to do this that allows me to keep snapshots at each level? I could manually keep track of a path of keys through the hierarchy, but would prefer to avoid the complexity if possible