firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

`snapshot.forEach()` fails on snapshots from `child_added` events when using `orderByChild()` queries

Open pokey opened this issue 2 months ago • 3 comments

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_added events on a query ordered by a child field
  • Iterate through the snapshot's children using snapshot.forEach()

What actually happened:

  • The child_added event 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 via get(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

pokey avatar Sep 22 '25 09:09 pokey

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

google-oss-bot avatar Sep 22 '25 09:09 google-oss-bot

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:

  • onChildAdded event is to notify you of one new child (or one initial child). The DataSnapshot passed to the callback represents only that single child node.
  • forEach method 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?

jbalidiong avatar Oct 24 '25 14:10 jbalidiong

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

pokey avatar Oct 24 '25 18:10 pokey